Merge branch 'dev/translationsAsPackages' into devel
Conflicts: src/octoprint/server/__init__.py
This commit is contained in:
commit
02b7085543
23 changed files with 3423 additions and 7642 deletions
5
setup.py
5
setup.py
|
|
@ -75,9 +75,10 @@ def get_cmdclass():
|
|||
cmdclass.update(dict(clean=octoprint_setuptools.CleanCommand.for_options(source_folder="src", eggs=["OctoPrint*.egg-info"])))
|
||||
|
||||
# add translation commands
|
||||
translation_dir = os.path.join("src", "octoprint", "translations")
|
||||
translation_dir = "translations"
|
||||
pot_file = os.path.join(translation_dir, "messages.pot")
|
||||
cmdclass.update(octoprint_setuptools.get_babel_commandclasses(pot_file=pot_file, output_dir=translation_dir))
|
||||
bundled_dir = os.path.join("src", "octoprint", "translations")
|
||||
cmdclass.update(octoprint_setuptools.get_babel_commandclasses(pot_file=pot_file, output_dir=translation_dir, pack_name_prefix="OctoPrint-i18n-", pack_path_prefix="", bundled_dir=bundled_dir))
|
||||
|
||||
return cmdclass
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
|
|||
|
||||
import uuid
|
||||
from sockjs.tornado import SockJSRouter
|
||||
from flask import Flask, render_template, send_from_directory, g, request, make_response, session, url_for
|
||||
from flask import Flask, g, request, session
|
||||
from flask.ext.login import LoginManager, current_user
|
||||
from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed
|
||||
from flask.ext.babel import Babel, gettext, ngettext
|
||||
|
|
@ -24,7 +24,7 @@ SUCCESS = {}
|
|||
NO_CONTENT = ("", 204)
|
||||
|
||||
app = Flask("octoprint")
|
||||
babel = Babel(app)
|
||||
babel = None
|
||||
debug = False
|
||||
|
||||
printer = None
|
||||
|
|
@ -58,9 +58,6 @@ import octoprint.filemanager.analysis
|
|||
import octoprint.slicing
|
||||
|
||||
from . import util
|
||||
util.tornado.fix_ioloop_scheduling()
|
||||
util.flask.enable_plugin_translations()
|
||||
|
||||
|
||||
UI_API_KEY = ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes)
|
||||
|
||||
|
|
@ -70,519 +67,8 @@ BRANCH = versions['branch'] if 'branch' in versions else None
|
|||
DISPLAY_VERSION = "%s (%s branch)" % (VERSION, BRANCH) if BRANCH else VERSION
|
||||
del versions
|
||||
|
||||
|
||||
def get_available_locale_identifiers(locales):
|
||||
result = set()
|
||||
|
||||
# add available translations
|
||||
for locale in locales:
|
||||
result.add(locale.language)
|
||||
if locale.territory:
|
||||
# if a territory is specified, add that too
|
||||
result.add("%s_%s" % (locale.language, locale.territory))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
LOCALES = [Locale.parse("en")] + babel.list_translations()
|
||||
LANGUAGES = get_available_locale_identifiers(LOCALES)
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.locale = get_locale()
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
# send no-cache headers with all POST responses
|
||||
if request.method == "POST":
|
||||
response.cache_control.no_cache = True
|
||||
response.headers.add("X-Clacks-Overhead", "GNU Terry Pratchett")
|
||||
return response
|
||||
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
if "l10n" in request.values:
|
||||
return Locale.negotiate([request.values["l10n"]], LANGUAGES)
|
||||
|
||||
if hasattr(g, "identity") and g.identity and userManager is not None:
|
||||
userid = g.identity.id
|
||||
try:
|
||||
user_language = userManager.getUserSetting(userid, ("interface", "language"))
|
||||
if user_language is not None and not user_language == "_default":
|
||||
return Locale.negotiate([user_language], LANGUAGES)
|
||||
except octoprint.users.UnknownUser:
|
||||
pass
|
||||
|
||||
default_language = settings().get(["appearance", "defaultLanguage"])
|
||||
if default_language is not None and not default_language == "_default" and default_language in LANGUAGES:
|
||||
return Locale.negotiate([default_language], LANGUAGES)
|
||||
|
||||
return request.accept_languages.best_match(LANGUAGES)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, key=lambda: "view/%s/%s" % (request.path, g.locale))
|
||||
def index():
|
||||
|
||||
#~~ a bunch of settings
|
||||
|
||||
enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"])
|
||||
enable_timelapse = (settings().get(["webcam", "snapshot"]) and settings().get(["webcam", "ffmpeg"]))
|
||||
enable_systemmenu = settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0
|
||||
enable_accesscontrol = userManager is not None
|
||||
preferred_stylesheet = settings().get(["devel", "stylesheet"])
|
||||
locales = dict((l.language, dict(language=l.language, display=l.display_name, english=l.english_name)) for l in LOCALES)
|
||||
|
||||
#~~ prepare assets
|
||||
|
||||
supported_stylesheets = ("css", "less")
|
||||
assets = dict(
|
||||
js=[],
|
||||
stylesheets=[]
|
||||
)
|
||||
assets["js"] = [
|
||||
url_for('static', filename='js/app/viewmodels/appearance.js'),
|
||||
url_for('static', filename='js/app/viewmodels/connection.js'),
|
||||
url_for('static', filename='js/app/viewmodels/control.js'),
|
||||
url_for('static', filename='js/app/viewmodels/firstrun.js'),
|
||||
url_for('static', filename='js/app/viewmodels/files.js'),
|
||||
url_for('static', filename='js/app/viewmodels/loginstate.js'),
|
||||
url_for('static', filename='js/app/viewmodels/navigation.js'),
|
||||
url_for('static', filename='js/app/viewmodels/printerstate.js'),
|
||||
url_for('static', filename='js/app/viewmodels/printerprofiles.js'),
|
||||
url_for('static', filename='js/app/viewmodels/settings.js'),
|
||||
url_for('static', filename='js/app/viewmodels/slicing.js'),
|
||||
url_for('static', filename='js/app/viewmodels/temperature.js'),
|
||||
url_for('static', filename='js/app/viewmodels/terminal.js'),
|
||||
url_for('static', filename='js/app/viewmodels/users.js'),
|
||||
url_for('static', filename='js/app/viewmodels/log.js'),
|
||||
url_for('static', filename='js/app/viewmodels/usersettings.js')
|
||||
]
|
||||
if enable_gcodeviewer:
|
||||
assets["js"] += [
|
||||
url_for('static', filename='js/app/viewmodels/gcode.js'),
|
||||
url_for('static', filename='gcodeviewer/js/ui.js'),
|
||||
url_for('static', filename='gcodeviewer/js/gCodeReader.js'),
|
||||
url_for('static', filename='gcodeviewer/js/renderer.js')
|
||||
]
|
||||
if enable_timelapse:
|
||||
assets["js"].append(url_for('static', filename='js/app/viewmodels/timelapse.js'))
|
||||
|
||||
if preferred_stylesheet == "less":
|
||||
assets["stylesheets"].append(("less", url_for('static', filename='less/octoprint.less')))
|
||||
elif preferred_stylesheet == "css":
|
||||
assets["stylesheets"].append(("css", url_for('static', filename='css/octoprint.css')))
|
||||
|
||||
asset_plugins = pluginManager.get_implementations(octoprint.plugin.AssetPlugin)
|
||||
for implementation in asset_plugins:
|
||||
name = implementation._identifier
|
||||
all_assets = implementation.get_assets()
|
||||
|
||||
if "js" in all_assets:
|
||||
for asset in all_assets["js"]:
|
||||
assets["js"].append(url_for('plugin_assets', name=name, filename=asset))
|
||||
|
||||
if preferred_stylesheet in all_assets:
|
||||
for asset in all_assets[preferred_stylesheet]:
|
||||
assets["stylesheets"].append((preferred_stylesheet, url_for('plugin_assets', name=name, filename=asset)))
|
||||
else:
|
||||
for stylesheet in supported_stylesheets:
|
||||
if not stylesheet in all_assets:
|
||||
continue
|
||||
|
||||
for asset in all_assets[stylesheet]:
|
||||
assets["stylesheets"].append((stylesheet, url_for('plugin_assets', name=name, filename=asset)))
|
||||
break
|
||||
|
||||
##~~ prepare templates
|
||||
|
||||
templates = defaultdict(lambda: dict(order=[], entries=dict()))
|
||||
|
||||
# rules for transforming template configs to template entries
|
||||
template_rules = dict(
|
||||
navbar=dict(div=lambda x: "navbar_plugin_" + x, template=lambda x: x + "_navbar.jinja2", to_entry=lambda data: data),
|
||||
sidebar=dict(div=lambda x: "sidebar_plugin_" + x, template=lambda x: x + "_sidebar.jinja2", to_entry=lambda data: (data["name"], data)),
|
||||
tab=dict(div=lambda x: "tab_plugin_" + x, template=lambda x: x + "_tab.jinja2", to_entry=lambda data: (data["name"], data)),
|
||||
settings=dict(div=lambda x: "settings_plugin_" + x, template=lambda x: x + "_settings.jinja2", to_entry=lambda data: (data["name"], data)),
|
||||
usersettings=dict(div=lambda x: "usersettings_plugin_" + x, template=lambda x: x + "_usersettings.jinja2", to_entry=lambda data: (data["name"], data)),
|
||||
generic=dict(template=lambda x: x + ".jinja2", to_entry=lambda data: data)
|
||||
)
|
||||
|
||||
# sorting orders
|
||||
template_sorting = dict(
|
||||
navbar=dict(add="prepend", key=None),
|
||||
sidebar=dict(add="append", key="name"),
|
||||
tab=dict(add="append", key="name"),
|
||||
settings=dict(add="custom_append", key="name", custom_add_entries=lambda missing: dict(section_plugins=(gettext("Plugins"), None)), custom_add_order=lambda missing: ["section_plugins"] + missing),
|
||||
usersettings=dict(add="append", key="name"),
|
||||
generic=dict(add="append", key=None)
|
||||
)
|
||||
|
||||
hooks = pluginManager.get_hooks("octoprint.ui.web.templatetypes")
|
||||
for name, hook in hooks.items():
|
||||
try:
|
||||
result = hook(dict(template_rules))
|
||||
except:
|
||||
# TODO
|
||||
pass
|
||||
else:
|
||||
if not isinstance(result, list):
|
||||
continue
|
||||
|
||||
for entry in result:
|
||||
if not isinstance(entry, tuple) or not len(entry) == 3:
|
||||
continue
|
||||
|
||||
key, order, rule = entry
|
||||
|
||||
# order defaults
|
||||
if "add" not in order:
|
||||
order["add"] = "prepend"
|
||||
if "key" not in order:
|
||||
order["key"] = "name"
|
||||
|
||||
# rule defaults
|
||||
if "div" not in rule:
|
||||
# default div name: <hook plugin>_<template_key>_plugin_<plugin>
|
||||
div = "{name}_{key}_plugin_".format(**locals())
|
||||
rule["div"] = lambda x: div + x
|
||||
if "template" not in rule:
|
||||
# default template name: <plugin>_plugin_<hook plugin>_<template key>.jinja2
|
||||
template = "_plugin_{name}_{key}.jinja2".format(**locals())
|
||||
rule["template"] = lambda x: x + template
|
||||
if "to_entry" not in rule:
|
||||
# default to_entry assumes existing "name" property to be used as label for 2-tuple entry data structure (<name>, <properties>)
|
||||
rule["to_entry"] = lambda data: (data["name"], data)
|
||||
|
||||
template_rules["plugin_" + name + "_" + key] = rule
|
||||
template_sorting["plugin_" + name + "_" + key] = order
|
||||
template_types = template_rules.keys()
|
||||
|
||||
# navbar
|
||||
|
||||
templates["navbar"]["entries"] = dict(
|
||||
settings=dict(template="navbar/settings.jinja2", _div="navbar_settings", styles=["display: none"], data_bind="visible: loginState.isAdmin")
|
||||
)
|
||||
if enable_accesscontrol:
|
||||
templates["navbar"]["entries"]["login"] = dict(template="navbar/login.jinja2", _div="navbar_login", classes=["dropdown"], custom_bindings=False)
|
||||
if enable_systemmenu:
|
||||
templates["navbar"]["entries"]["systemmenu"] = dict(template="navbar/systemmenu.jinja2", _div="navbar_systemmenu", styles=["display: none"], classes=["dropdown"], data_bind="visible: loginState.isAdmin", custom_bindings=False)
|
||||
|
||||
# sidebar
|
||||
|
||||
templates["sidebar"]["entries"]= dict(
|
||||
connection=(gettext("Connection"), dict(template="sidebar/connection.jinja2", _div="connection", icon="signal", styles_wrapper=["display: none"], data_bind="visible: loginState.isAdmin")),
|
||||
state=(gettext("State"), dict(template="sidebar/state.jinja2", _div="state", icon="info-sign")),
|
||||
files=(gettext("Files"), dict(template="sidebar/files.jinja2", _div="files", icon="list", classes_content=["overflow_visible"], template_header="sidebar/files_header.jinja2"))
|
||||
)
|
||||
|
||||
# tabs
|
||||
|
||||
templates["tab"]["entries"] = dict(
|
||||
temperature=(gettext("Temperature"), dict(template="tabs/temperature.jinja2", _div="temp")),
|
||||
control=(gettext("Control"), dict(template="tabs/control.jinja2", _div="control")),
|
||||
terminal=(gettext("Terminal"), dict(template="tabs/terminal.jinja2", _div="term")),
|
||||
)
|
||||
if enable_gcodeviewer:
|
||||
templates["tab"]["entries"]["gcodeviewer"] = (gettext("GCode Viewer"), dict(template="tabs/gcodeviewer.jinja2", _div="gcode"))
|
||||
if enable_timelapse:
|
||||
templates["tab"]["entries"]["timelapse"] = (gettext("Timelapse"), dict(template="tabs/timelapse.jinja2", _div="timelapse"))
|
||||
|
||||
# settings dialog
|
||||
|
||||
templates["settings"]["entries"] = dict(
|
||||
section_printer=(gettext("Printer"), None),
|
||||
|
||||
serial=(gettext("Serial Connection"), dict(template="dialogs/settings/serialconnection.jinja2", _div="settings_serialConnection", custom_bindings=False)),
|
||||
printerprofiles=(gettext("Printer Profiles"), dict(template="dialogs/settings/printerprofiles.jinja2", _div="settings_printerProfiles", custom_bindings=False)),
|
||||
temperatures=(gettext("Temperatures"), dict(template="dialogs/settings/temperatures.jinja2", _div="settings_temperature", custom_bindings=False)),
|
||||
terminalfilters=(gettext("Terminal Filters"), dict(template="dialogs/settings/terminalfilters.jinja2", _div="settings_terminalFilters", custom_bindings=False)),
|
||||
gcodescripts=(gettext("GCODE Scripts"), dict(template="dialogs/settings/gcodescripts.jinja2", _div="settings_gcodeScripts", custom_bindings=False)),
|
||||
|
||||
section_features=(gettext("Features"), None),
|
||||
|
||||
features=(gettext("Features"), dict(template="dialogs/settings/features.jinja2", _div="settings_features", custom_bindings=False)),
|
||||
webcam=(gettext("Webcam"), dict(template="dialogs/settings/webcam.jinja2", _div="settings_webcam", custom_bindings=False)),
|
||||
api=(gettext("API"), dict(template="dialogs/settings/api.jinja2", _div="settings_api", custom_bindings=False)),
|
||||
|
||||
section_octoprint=(gettext("OctoPrint"), None),
|
||||
|
||||
folders=(gettext("Folders"), dict(template="dialogs/settings/folders.jinja2", _div="settings_folders", custom_bindings=False)),
|
||||
appearance=(gettext("Appearance"), dict(template="dialogs/settings/appearance.jinja2", _div="settings_appearance", custom_bindings=False)),
|
||||
logs=(gettext("Logs"), dict(template="dialogs/settings/logs.jinja2", _div="settings_logs")),
|
||||
)
|
||||
if enable_accesscontrol:
|
||||
templates["settings"]["entries"]["accesscontrol"] = (gettext("Access Control"), dict(template="dialogs/settings/accesscontrol.jinja2", _div="settings_users", custom_bindings=False))
|
||||
|
||||
# user settings dialog
|
||||
|
||||
if enable_accesscontrol:
|
||||
templates["usersettings"]["entries"] = dict(
|
||||
access=(gettext("Access"), dict(template="dialogs/usersettings/access.jinja2", _div="usersettings_access", custom_bindings=False)),
|
||||
interface=(gettext("Interface"), dict(template="dialogs/usersettings/interface.jinja2", _div="usersettings_interface", custom_bindings=False)),
|
||||
)
|
||||
|
||||
# extract data from template plugins
|
||||
|
||||
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)
|
||||
|
||||
plugin_vars = dict()
|
||||
plugin_names = set()
|
||||
for implementation in template_plugins:
|
||||
name = implementation._identifier
|
||||
plugin_names.add(name)
|
||||
|
||||
vars = implementation.get_template_vars()
|
||||
if not isinstance(vars, dict):
|
||||
vars = dict()
|
||||
|
||||
for var_name, var_value in vars.items():
|
||||
plugin_vars["plugin_" + name + "_" + var_name] = var_value
|
||||
|
||||
configs = implementation.get_template_configs()
|
||||
if not isinstance(configs, (list, tuple)):
|
||||
configs = []
|
||||
|
||||
includes = _process_template_configs(name, implementation, configs, template_rules)
|
||||
|
||||
for t in template_types:
|
||||
for include in includes[t]:
|
||||
if t == "navbar" or t == "generic":
|
||||
data = include
|
||||
else:
|
||||
data = include[1]
|
||||
|
||||
key = data["_key"]
|
||||
if "replaces" in data:
|
||||
key = data["replaces"]
|
||||
templates[t]["entries"][key] = include
|
||||
|
||||
#~~ order internal templates and plugins
|
||||
|
||||
# make sure that
|
||||
# 1) we only have keys in our ordered list that we have entries for and
|
||||
# 2) we have all entries located somewhere within the order
|
||||
|
||||
for t in template_types:
|
||||
default_order = settings().get(["appearance", "components", "order", t], merged=True, config=dict()) or []
|
||||
configured_order = settings().get(["appearance", "components", "order", t], merged=True) or []
|
||||
configured_disabled = settings().get(["appearance", "components", "disabled", t]) or []
|
||||
|
||||
# first create the ordered list of all component ids according to the configured order
|
||||
templates[t]["order"] = [x for x in configured_order if x in templates[t]["entries"] and not x in configured_disabled]
|
||||
|
||||
# now append the entries from the default order that are not already in there
|
||||
templates[t]["order"] += [x for x in default_order if not x in templates[t]["order"] and x in templates[t]["entries"] and not x in configured_disabled]
|
||||
|
||||
all_ordered = set(templates[t]["order"])
|
||||
all_disabled = set(configured_disabled)
|
||||
|
||||
# check if anything is missing, if not we are done here
|
||||
missing_in_order = set(templates[t]["entries"].keys()).difference(all_ordered).difference(all_disabled)
|
||||
if len(missing_in_order) == 0:
|
||||
continue
|
||||
|
||||
# finally add anything that's not included in our order yet
|
||||
sorted_missing = list(missing_in_order)
|
||||
if template_sorting[t]["key"] is not None:
|
||||
# anything but navbar and generic components get sorted by their name
|
||||
if template_sorting[t]["key"] == "name":
|
||||
sorted_missing = sorted(missing_in_order, key=lambda x: templates[t]["entries"][x][0])
|
||||
|
||||
if template_sorting[t]["add"] == "prepend":
|
||||
templates[t]["order"] = sorted_missing + templates[t]["order"]
|
||||
elif template_sorting[t]["add"] == "append":
|
||||
templates[t]["order"] += sorted_missing
|
||||
elif template_sorting[t]["add"] == "custom_prepend" and "custom_add_entries" in template_sorting[t] and "custom_add_order" in template_sorting[t]:
|
||||
templates[t]["entries"].update(template_sorting[t]["custom_add_entries"](sorted_missing))
|
||||
templates[t]["order"] = template_sorting[t]["custom_add_order"](sorted_missing) + templates[t]["order"]
|
||||
elif template_sorting[t]["add"] == "custom_append" and "custom_add_entries" in template_sorting[t] and "custom_add_order" in template_sorting[t]:
|
||||
templates[t]["entries"].update(template_sorting[t]["custom_add_entries"](sorted_missing))
|
||||
templates[t]["order"] += template_sorting[t]["custom_add_order"](sorted_missing)
|
||||
|
||||
#~~ prepare full set of template vars for rendering
|
||||
|
||||
render_kwargs = dict(
|
||||
webcamStream=settings().get(["webcam", "stream"]),
|
||||
enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
|
||||
enableAccessControl=userManager is not None,
|
||||
enableSdSupport=settings().get(["feature", "sdSupport"]),
|
||||
firstRun=settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()),
|
||||
debug=debug,
|
||||
version=VERSION,
|
||||
display_version=DISPLAY_VERSION,
|
||||
gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]),
|
||||
gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]),
|
||||
uiApiKey=UI_API_KEY,
|
||||
templates=templates,
|
||||
assets=assets,
|
||||
pluginNames=plugin_names,
|
||||
locales=locales
|
||||
)
|
||||
render_kwargs.update(plugin_vars)
|
||||
|
||||
#~~ render!
|
||||
|
||||
return render_template(
|
||||
"index.jinja2",
|
||||
**render_kwargs
|
||||
)
|
||||
|
||||
|
||||
def _process_template_configs(name, implementation, configs, rules):
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
counters = defaultdict(lambda: 1)
|
||||
includes = defaultdict(list)
|
||||
|
||||
for config in configs:
|
||||
if not isinstance(config, dict):
|
||||
continue
|
||||
if not "type" in config:
|
||||
continue
|
||||
|
||||
template_type = config["type"]
|
||||
del config["type"]
|
||||
|
||||
if not template_type in rules:
|
||||
continue
|
||||
rule = rules[template_type]
|
||||
|
||||
data = _process_template_config(name, implementation, rule, config=config, counter=counters[template_type])
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
includes[template_type].append(rule["to_entry"](data))
|
||||
counters[template_type] += 1
|
||||
|
||||
for template_type in rules:
|
||||
if len(includes[template_type]) == 0:
|
||||
# if no template of that type was added by the config, we'll try to use the default template name
|
||||
rule = rules[template_type]
|
||||
data = _process_template_config(name, implementation, rule)
|
||||
if data is not None:
|
||||
try:
|
||||
app.jinja_env.get_or_select_template(data["template"])
|
||||
except TemplateNotFound:
|
||||
pass
|
||||
else:
|
||||
includes[template_type].append(rule["to_entry"](data))
|
||||
|
||||
return includes
|
||||
|
||||
def _process_template_config(name, implementation, rule, config=None, counter=1):
|
||||
if "mandatory" in rule:
|
||||
for mandatory in rule["mandatory"]:
|
||||
if not mandatory in config:
|
||||
return None
|
||||
|
||||
if config is None:
|
||||
config = dict()
|
||||
data = dict(config)
|
||||
|
||||
if not "suffix" in data and counter > 1:
|
||||
data["suffix"] = "_%d" % counter
|
||||
|
||||
if "div" in data:
|
||||
data["_div"] = data["div"]
|
||||
elif "div" in rule:
|
||||
data["_div"] = rule["div"](name)
|
||||
if "suffix" in data:
|
||||
data["_div"] = data["_div"] + data["suffix"]
|
||||
|
||||
if not "template" in data:
|
||||
data["template"] = rule["template"](name)
|
||||
|
||||
if not "name" in data:
|
||||
data["name"] = implementation._plugin_name
|
||||
|
||||
if not "custom_bindings" in data or data["custom_bindings"]:
|
||||
data_bind = "allowBindings: true"
|
||||
if "data_bind" in data:
|
||||
data_bind = data_bind + ", " + data["data_bind"]
|
||||
data["data_bind"] = data_bind
|
||||
|
||||
data["_key"] = "plugin_" + name
|
||||
if "suffix" in data:
|
||||
data["_key"] += data["suffix"]
|
||||
|
||||
return data
|
||||
|
||||
@app.route("/robots.txt")
|
||||
def robotsTxt():
|
||||
return send_from_directory(app.static_folder, "robots.txt")
|
||||
|
||||
|
||||
@app.route("/i18n/<string:locale>/<string:domain>.js")
|
||||
@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, key=lambda: "view/%s/%s" % (request.path, g.locale))
|
||||
def localeJs(locale, domain):
|
||||
from flask import _request_ctx_stack
|
||||
from babel.messages.pofile import read_po
|
||||
|
||||
def messages_from_po(base_path, locale, domain):
|
||||
path = os.path.join(base_path, locale)
|
||||
if not os.path.isdir(path):
|
||||
return dict(), None
|
||||
|
||||
path = os.path.join(path, "LC_MESSAGES", "{domain}.po".format(**locals()))
|
||||
if not os.path.isfile(path):
|
||||
return dict(), None
|
||||
|
||||
messages = dict()
|
||||
with file(path) as f:
|
||||
catalog = read_po(f, locale=locale, domain=domain)
|
||||
|
||||
for message in catalog:
|
||||
message_id = message.id
|
||||
if isinstance(message_id, (list, tuple)):
|
||||
message_id = message_id[0]
|
||||
messages[message_id] = message.string
|
||||
|
||||
return messages, catalog.plural_expr
|
||||
|
||||
messages = dict()
|
||||
|
||||
ctx = _request_ctx_stack.top
|
||||
base_path = os.path.join(ctx.app.root_path, "translations")
|
||||
|
||||
plugins = octoprint.plugin.plugin_manager().enabled_plugins
|
||||
for name, plugin in plugins.items():
|
||||
plugin_path = os.path.join(plugin.location, 'translations')
|
||||
plugin_messages, _ = messages_from_po(plugin_path, locale, domain)
|
||||
messages = octoprint.util.dict_merge(messages, plugin_messages)
|
||||
|
||||
core_messages, plural_expr = messages_from_po(base_path, locale, domain)
|
||||
messages = octoprint.util.dict_merge(messages, core_messages)
|
||||
|
||||
catalog = dict(
|
||||
messages=messages,
|
||||
plural_expr=plural_expr,
|
||||
locale=locale,
|
||||
domain=domain
|
||||
)
|
||||
|
||||
return render_template("i18n.js.jinja2", catalog=catalog)
|
||||
|
||||
|
||||
@app.route("/plugin_assets/<string:name>/<path:filename>")
|
||||
def plugin_assets(name, filename):
|
||||
asset_plugins = pluginManager.get_filtered_implementations(lambda p: p._identifier == name, octoprint.plugin.AssetPlugin)
|
||||
|
||||
if not asset_plugins:
|
||||
return make_response("Asset not found", 404)
|
||||
|
||||
if len(asset_plugins) > 1:
|
||||
return make_response("More than one asset provider for {name}, can't proceed".format(name=name), 500)
|
||||
|
||||
asset_plugin = asset_plugins[0]
|
||||
asset_folder = asset_plugin.get_asset_folder()
|
||||
if asset_folder is None:
|
||||
return make_response("Asset not found", 404)
|
||||
|
||||
return send_from_directory(asset_folder, filename)
|
||||
|
||||
LOCALES = []
|
||||
LANGUAGES = set()
|
||||
|
||||
@identity_loaded.connect_via(app)
|
||||
def on_identity_loaded(sender, identity):
|
||||
|
|
@ -596,7 +82,6 @@ def on_identity_loaded(sender, identity):
|
|||
if user.is_admin():
|
||||
identity.provides.add(RoleNeed("admin"))
|
||||
|
||||
|
||||
def load_user(id):
|
||||
if id == "_api":
|
||||
return users.ApiUser()
|
||||
|
|
@ -638,6 +123,9 @@ class Server():
|
|||
if not self._allowRoot:
|
||||
self._check_for_root()
|
||||
|
||||
global app
|
||||
global babel
|
||||
|
||||
global printer
|
||||
global printerProfileManager
|
||||
global fileManager
|
||||
|
|
@ -658,7 +146,17 @@ class Server():
|
|||
debug = self._debug
|
||||
|
||||
# first initialize the settings singleton and make sure it uses given configfile and basedir if available
|
||||
settings(init=True, basedir=self._basedir, configfile=self._configfile)
|
||||
s = settings(init=True, basedir=self._basedir, configfile=self._configfile)
|
||||
|
||||
# then monkey patch a bunch of stuff
|
||||
util.tornado.fix_ioloop_scheduling()
|
||||
util.flask.enable_additional_translations(additional_folders=[s.getBaseFolder("translations")])
|
||||
|
||||
# setup app
|
||||
self._setup_app()
|
||||
|
||||
# setup i18n
|
||||
self._setup_i18n(app)
|
||||
|
||||
# then initialize logging
|
||||
self._setup_logging(self._debug, self._logConf)
|
||||
|
|
@ -674,9 +172,9 @@ class Server():
|
|||
printerProfileManager = PrinterProfileManager()
|
||||
eventManager = events.eventManager()
|
||||
analysisQueue = octoprint.filemanager.analysis.AnalysisQueue()
|
||||
slicingManager = octoprint.slicing.SlicingManager(settings().getBaseFolder("slicingProfiles"), printerProfileManager)
|
||||
slicingManager = octoprint.slicing.SlicingManager(s.getBaseFolder("slicingProfiles"), printerProfileManager)
|
||||
storage_managers = dict()
|
||||
storage_managers[octoprint.filemanager.FileDestinations.LOCAL] = octoprint.filemanager.storage.LocalFileStorage(settings().getBaseFolder("uploads"))
|
||||
storage_managers[octoprint.filemanager.FileDestinations.LOCAL] = octoprint.filemanager.storage.LocalFileStorage(s.getBaseFolder("uploads"))
|
||||
fileManager = octoprint.filemanager.FileManager(analysisQueue, slicingManager, printerProfileManager, initial_storage_managers=storage_managers)
|
||||
printer = Printer(fileManager, analysisQueue, printerProfileManager)
|
||||
appSessionManager = util.flask.AppSessionManager()
|
||||
|
|
@ -742,8 +240,8 @@ class Server():
|
|||
if self._debug:
|
||||
events.DebugEventListener()
|
||||
|
||||
if settings().getBoolean(["accessControl", "enabled"]):
|
||||
userManagerName = settings().get(["accessControl", "userManager"])
|
||||
if s.getBoolean(["accessControl", "enabled"]):
|
||||
userManagerName = s.get(["accessControl", "userManager"])
|
||||
try:
|
||||
clazz = octoprint.util.get_class(userManagerName)
|
||||
userManager = clazz()
|
||||
|
|
@ -752,22 +250,22 @@ class Server():
|
|||
|
||||
app.wsgi_app = util.ReverseProxied(
|
||||
app.wsgi_app,
|
||||
settings().get(["server", "reverseProxy", "prefixHeader"]),
|
||||
settings().get(["server", "reverseProxy", "schemeHeader"]),
|
||||
settings().get(["server", "reverseProxy", "hostHeader"]),
|
||||
settings().get(["server", "reverseProxy", "prefixFallback"]),
|
||||
settings().get(["server", "reverseProxy", "schemeFallback"]),
|
||||
settings().get(["server", "reverseProxy", "hostFallback"])
|
||||
s.get(["server", "reverseProxy", "prefixHeader"]),
|
||||
s.get(["server", "reverseProxy", "schemeHeader"]),
|
||||
s.get(["server", "reverseProxy", "hostHeader"]),
|
||||
s.get(["server", "reverseProxy", "prefixFallback"]),
|
||||
s.get(["server", "reverseProxy", "schemeFallback"]),
|
||||
s.get(["server", "reverseProxy", "hostFallback"])
|
||||
)
|
||||
|
||||
secret_key = settings().get(["server", "secretKey"])
|
||||
secret_key = s.get(["server", "secretKey"])
|
||||
if not secret_key:
|
||||
import string
|
||||
from random import choice
|
||||
chars = string.ascii_lowercase + string.ascii_uppercase + string.digits
|
||||
secret_key = "".join(choice(chars) for _ in xrange(32))
|
||||
settings().set(["server", "secretKey"], secret_key)
|
||||
settings().save()
|
||||
s.set(["server", "secretKey"], secret_key)
|
||||
s.save()
|
||||
app.secret_key = secret_key
|
||||
loginManager = LoginManager()
|
||||
loginManager.session_protection = "strong"
|
||||
|
|
@ -778,9 +276,9 @@ class Server():
|
|||
loginManager.init_app(app)
|
||||
|
||||
if self._host is None:
|
||||
self._host = settings().get(["server", "host"])
|
||||
self._host = s.get(["server", "host"])
|
||||
if self._port is None:
|
||||
self._port = settings().getInt(["server", "port"])
|
||||
self._port = s.getInt(["server", "port"])
|
||||
|
||||
app.debug = self._debug
|
||||
|
||||
|
|
@ -799,13 +297,13 @@ class Server():
|
|||
|
||||
self._router = SockJSRouter(self._create_socket_connection, "/sockjs")
|
||||
|
||||
upload_suffixes = dict(name=settings().get(["server", "uploads", "nameSuffix"]), path=settings().get(["server", "uploads", "pathSuffix"]))
|
||||
upload_suffixes = dict(name=s.get(["server", "uploads", "nameSuffix"]), path=s.get(["server", "uploads", "pathSuffix"]))
|
||||
|
||||
server_routes = self._router.urls + [
|
||||
(r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, dict(path=settings().getBaseFolder("timelapse"), as_attachment=True)),
|
||||
(r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, dict(path=settings().getBaseFolder("uploads"), as_attachment=True, path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))),
|
||||
(r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, dict(path=settings().getBaseFolder("logs"), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))),
|
||||
(r"/downloads/camera/current", util.tornado.UrlForwardHandler, dict(url=settings().get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))),
|
||||
(r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("timelapse"), as_attachment=True)),
|
||||
(r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("uploads"), as_attachment=True, path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))),
|
||||
(r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("logs"), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))),
|
||||
(r"/downloads/camera/current", util.tornado.UrlForwardHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))),
|
||||
]
|
||||
for name, hook in pluginManager.get_hooks("octoprint.server.http.routes").items():
|
||||
try:
|
||||
|
|
@ -832,7 +330,8 @@ class Server():
|
|||
|
||||
self._tornado_app = Application(server_routes)
|
||||
max_body_sizes = [
|
||||
("POST", r"/api/files/([^/]*)", settings().getInt(["server", "uploads", "maxSize"]))
|
||||
("POST", r"/api/files/([^/]*)", s.getInt(["server", "uploads", "maxSize"])),
|
||||
("POST", r"/api/languages", 5 * 1024 * 1024)
|
||||
]
|
||||
|
||||
# allow plugins to extend allowed maximum body sizes
|
||||
|
|
@ -857,12 +356,12 @@ class Server():
|
|||
self._logger.debug("Adding maximum body size of {size}B for {method} requests to {route})".format(**locals()))
|
||||
max_body_sizes.append((method, route, size))
|
||||
|
||||
self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=settings().getInt(["server", "maxSize"]))
|
||||
self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=s.getInt(["server", "maxSize"]))
|
||||
self._server.listen(self._port, address=self._host)
|
||||
|
||||
eventManager.fire(events.Events.STARTUP)
|
||||
if settings().getBoolean(["serial", "autoconnect"]):
|
||||
(port, baudrate) = settings().get(["serial", "port"]), settings().getInt(["serial", "baudrate"])
|
||||
if s.getBoolean(["serial", "autoconnect"]):
|
||||
(port, baudrate) = s.get(["serial", "port"]), s.getInt(["serial", "baudrate"])
|
||||
printer_profile = printerProfileManager.get_default()
|
||||
connectionOptions = get_connection_options()
|
||||
if port in connectionOptions["ports"]:
|
||||
|
|
@ -870,7 +369,7 @@ class Server():
|
|||
|
||||
# start up watchdogs
|
||||
observer = Observer()
|
||||
observer.schedule(util.watchdog.GcodeWatchdogHandler(fileManager, printer), settings().getBaseFolder("watched"))
|
||||
observer.schedule(util.watchdog.GcodeWatchdogHandler(fileManager, printer), s.getBaseFolder("watched"))
|
||||
observer.start()
|
||||
|
||||
# run our startup plugins
|
||||
|
|
@ -934,6 +433,27 @@ class Server():
|
|||
if "geteuid" in dir(os) and os.geteuid() == 0:
|
||||
exit("You should not run OctoPrint as root!")
|
||||
|
||||
def _get_locale(self):
|
||||
global LANGUAGES
|
||||
|
||||
if "l10n" in request.values:
|
||||
return Locale.negotiate([request.values["l10n"]], LANGUAGES)
|
||||
|
||||
if hasattr(g, "identity") and g.identity and userManager is not None:
|
||||
userid = g.identity.id
|
||||
try:
|
||||
user_language = userManager.getUserSetting(userid, ("interface", "language"))
|
||||
if user_language is not None and not user_language == "_default":
|
||||
return Locale.negotiate([user_language], LANGUAGES)
|
||||
except octoprint.users.UnknownUser:
|
||||
pass
|
||||
|
||||
default_language = settings().get(["appearance", "defaultLanguage"])
|
||||
if default_language is not None and not default_language == "_default" and default_language in LANGUAGES:
|
||||
return Locale.negotiate([default_language], LANGUAGES)
|
||||
|
||||
return request.accept_languages.best_match(LANGUAGES)
|
||||
|
||||
def _setup_logging(self, debug, logConf=None):
|
||||
defaultConfig = {
|
||||
"version": 1,
|
||||
|
|
@ -1008,6 +528,45 @@ class Server():
|
|||
logging.getLogger("SERIAL").setLevel(logging.DEBUG)
|
||||
logging.getLogger("SERIAL").debug("Enabling serial logging")
|
||||
|
||||
def _setup_app(self):
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.locale = self._get_locale()
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
# send no-cache headers with all POST responses
|
||||
if request.method == "POST":
|
||||
response.cache_control.no_cache = True
|
||||
response.headers.add("X-Clacks-Overhead", "GNU Terry Pratchett")
|
||||
return response
|
||||
|
||||
def _setup_i18n(self, app):
|
||||
global babel
|
||||
global LOCALES
|
||||
global LANGUAGES
|
||||
|
||||
babel = Babel(app)
|
||||
|
||||
def get_available_locale_identifiers(locales):
|
||||
result = set()
|
||||
|
||||
# add available translations
|
||||
for locale in locales:
|
||||
result.add(locale.language)
|
||||
if locale.territory:
|
||||
# if a territory is specified, add that too
|
||||
result.add("%s_%s" % (locale.language, locale.territory))
|
||||
|
||||
return result
|
||||
|
||||
LOCALES = babel.list_translations()
|
||||
LANGUAGES = get_available_locale_identifiers(LOCALES)
|
||||
|
||||
@babel.localeselector
|
||||
def get_locale():
|
||||
return self._get_locale()
|
||||
|
||||
def _setup_jinja2(self):
|
||||
app.jinja_env.add_extension("jinja2.ext.do")
|
||||
|
||||
|
|
@ -1043,6 +602,7 @@ class Server():
|
|||
def _setup_blueprints(self):
|
||||
from octoprint.server.api import api
|
||||
from octoprint.server.apps import apps
|
||||
import octoprint.server.views
|
||||
|
||||
app.register_blueprint(api, url_prefix="/api")
|
||||
app.register_blueprint(apps, url_prefix="/apps")
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ from . import users as api_users
|
|||
from . import log as api_logs
|
||||
from . import slicing as api_slicing
|
||||
from . import printer_profiles as api_printer_profiles
|
||||
from . import languages as api_languages
|
||||
|
||||
|
||||
VERSION = "0.1"
|
||||
|
|
|
|||
147
src/octoprint/server/api/languages.py
Normal file
147
src/octoprint/server/api/languages.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
import os
|
||||
import tarfile
|
||||
import zipfile
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from flask import request, jsonify, make_response
|
||||
|
||||
from octoprint.settings import settings
|
||||
|
||||
from octoprint.server import admin_permission
|
||||
from octoprint.server.api import api
|
||||
from octoprint.server.util.flask import restricted_access
|
||||
|
||||
from octoprint.plugin import plugin_manager
|
||||
|
||||
from flask.ext.babel import Locale
|
||||
|
||||
@api.route("/languages", methods=["GET"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def getInstalledLanguagePacks():
|
||||
translation_folder = settings().getBaseFolder("translations")
|
||||
if not os.path.exists(translation_folder):
|
||||
return jsonify(language_packs=dict(_core=[]))
|
||||
|
||||
core_packs = []
|
||||
plugin_packs = defaultdict(lambda: dict(identifier=None, display=None, languages=[]))
|
||||
for folder in os.listdir(translation_folder):
|
||||
path = os.path.join(translation_folder, folder)
|
||||
|
||||
if not os.path.isdir(path):
|
||||
continue
|
||||
|
||||
def load_meta(path, locale):
|
||||
meta = dict()
|
||||
|
||||
meta_path = os.path.join(path, "meta.yaml")
|
||||
if os.path.isfile(meta_path):
|
||||
import yaml
|
||||
try:
|
||||
with open(meta_path) as f:
|
||||
meta = yaml.safe_load(f)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
import datetime
|
||||
if "last_update" in meta and isinstance(meta["last_update"], datetime.datetime):
|
||||
meta["last_update"] = (meta["last_update"] - datetime.datetime(1970,1,1)).total_seconds()
|
||||
|
||||
l = Locale.parse(locale)
|
||||
meta["locale"] = locale
|
||||
meta["locale_display"] = l.display_name
|
||||
meta["locale_english"] = l.english_name
|
||||
return meta
|
||||
|
||||
if folder == "_plugins":
|
||||
for plugin_folder in os.listdir(path):
|
||||
plugin_path = os.path.join(path, plugin_folder)
|
||||
if not os.path.isdir(plugin_path):
|
||||
continue
|
||||
|
||||
if not plugin_folder in plugin_manager().plugins:
|
||||
continue
|
||||
|
||||
plugin_info = plugin_manager().plugins[plugin_folder]
|
||||
|
||||
plugin_packs[plugin_folder]["identifier"] = plugin_folder
|
||||
plugin_packs[plugin_folder]["display"] = plugin_info.name
|
||||
|
||||
for language_folder in os.listdir(plugin_path):
|
||||
plugin_packs[plugin_folder]["languages"].append(load_meta(os.path.join(plugin_path, language_folder), language_folder))
|
||||
else:
|
||||
core_packs.append(load_meta(os.path.join(translation_folder, folder), folder))
|
||||
|
||||
result = dict(_core=dict(identifier="_core", display="Core", languages=core_packs))
|
||||
result.update(plugin_packs)
|
||||
return jsonify(language_packs=result)
|
||||
|
||||
@api.route("/languages", methods=["POST"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def uploadLanguagePack():
|
||||
input_name = "file"
|
||||
input_upload_path = input_name + "." + settings().get(["server", "uploads", "pathSuffix"])
|
||||
if not input_upload_path in request.values:
|
||||
return make_response("No file included", 400)
|
||||
|
||||
upload_path = request.values[input_upload_path]
|
||||
|
||||
target_path = settings().getBaseFolder("translations")
|
||||
|
||||
if tarfile.is_tarfile(upload_path):
|
||||
_unpack_uploaded_tarball(upload_path, target_path)
|
||||
elif zipfile.is_zipfile(upload_path):
|
||||
_unpack_uploaded_zipfile(upload_path, target_path)
|
||||
else:
|
||||
return make_response("Neither zip file nor tarball included", 400)
|
||||
|
||||
return getInstalledLanguagePacks()
|
||||
|
||||
@api.route("/languages/<string:locale>/<string:pack>", methods=["DELETE"])
|
||||
@restricted_access
|
||||
@admin_permission.require(403)
|
||||
def deleteInstalledLanguagePack(locale, pack):
|
||||
|
||||
if pack == "_core":
|
||||
target_path = os.path.join(settings().getBaseFolder("translations"), locale)
|
||||
else:
|
||||
target_path = os.path.join(settings().getBaseFolder("translations"), "_plugins", pack, locale)
|
||||
|
||||
if os.path.isdir(target_path):
|
||||
import shutil
|
||||
shutil.rmtree(target_path)
|
||||
|
||||
return getInstalledLanguagePacks()
|
||||
|
||||
def _unpack_uploaded_zipfile(path, target):
|
||||
with zipfile.ZipFile(path, "r") as zip:
|
||||
# sanity check
|
||||
map(_validate_archive_name, zip.namelist())
|
||||
|
||||
# unpack everything
|
||||
zip.extractall(target)
|
||||
|
||||
def _unpack_uploaded_tarball(path, target):
|
||||
with tarfile.open(path, "r") as tar:
|
||||
# sanity check
|
||||
map(_validate_archive_name, tar.getmembers())
|
||||
|
||||
# unpack everything
|
||||
tar.extractall(target)
|
||||
|
||||
def _validate_archive_name(name):
|
||||
if name.startswith("/") or ".." in name:
|
||||
raise InvalidLanguagePack("Provided language pack contains invalid name {name}".format(**locals()))
|
||||
|
||||
|
||||
class InvalidLanguagePack(BaseException):
|
||||
pass
|
||||
|
|
@ -26,22 +26,52 @@ from werkzeug.contrib.cache import SimpleCache
|
|||
|
||||
#~~ monkey patching
|
||||
|
||||
def enable_plugin_translations():
|
||||
def enable_additional_translations(default_locale="en", additional_folders=None):
|
||||
import os
|
||||
from flask import _request_ctx_stack
|
||||
from babel import support
|
||||
from babel import support, Locale
|
||||
import flask.ext.babel
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
if additional_folders is None:
|
||||
additional_folders = []
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def fixed_list_translations(self):
|
||||
"""Returns a list of all the locales translations exist for. The
|
||||
list returned will be filled with actual locale objects and not just
|
||||
strings.
|
||||
"""
|
||||
def list_translations(dirname):
|
||||
if not os.path.isdir(dirname):
|
||||
return []
|
||||
result = []
|
||||
for folder in os.listdir(dirname):
|
||||
locale_dir = os.path.join(dirname, folder, 'LC_MESSAGES')
|
||||
if not os.path.isdir(locale_dir):
|
||||
continue
|
||||
if filter(lambda x: x.endswith('.mo'), os.listdir(locale_dir)):
|
||||
result.append(Locale.parse(folder))
|
||||
if not result:
|
||||
result.append(Locale.parse(self._default_locale))
|
||||
return result
|
||||
|
||||
dirs = additional_folders + [os.path.join(self.app.root_path, 'translations')]
|
||||
|
||||
result = [Locale.parse(default_locale)]
|
||||
|
||||
for dir in dirs:
|
||||
result += list_translations(dir)
|
||||
return result
|
||||
|
||||
def fixed_get_translations():
|
||||
"""Returns the correct gettext translations that should be used for
|
||||
this request. This will never fail and return a dummy translation
|
||||
object if used outside of the request or if a translation cannot be
|
||||
found.
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ctx = _request_ctx_stack.top
|
||||
if ctx is None:
|
||||
return None
|
||||
|
|
@ -50,26 +80,42 @@ def enable_plugin_translations():
|
|||
locale = flask.ext.babel.get_locale()
|
||||
translations = support.Translations()
|
||||
|
||||
plugins = octoprint.plugin.plugin_manager().enabled_plugins
|
||||
for name, plugin in plugins.items():
|
||||
dirname = os.path.join(plugin.location, 'translations')
|
||||
if not os.path.isdir(dirname):
|
||||
continue
|
||||
if str(locale) != default_locale:
|
||||
# plugin translations
|
||||
plugins = octoprint.plugin.plugin_manager().enabled_plugins
|
||||
for name, plugin in plugins.items():
|
||||
dirs = map(lambda x: os.path.join(x, "_plugins", name), additional_folders) + [os.path.join(plugin.location, 'translations')]
|
||||
for dirname in dirs:
|
||||
if not os.path.isdir(dirname):
|
||||
continue
|
||||
|
||||
try:
|
||||
plugin_translations = support.Translations.load(dirname, [locale])
|
||||
except:
|
||||
logger.exception("Error while trying to load translations for plugin {name}".format(**locals()))
|
||||
try:
|
||||
plugin_translations = support.Translations.load(dirname, [locale])
|
||||
except:
|
||||
logger.exception("Error while trying to load translations for plugin {name}".format(**locals()))
|
||||
else:
|
||||
if isinstance(plugin_translations, support.Translations):
|
||||
translations = translations.merge(plugin_translations)
|
||||
logger.debug("Using translation folder {dirname} for locale {locale} of plugin {name}".format(**locals()))
|
||||
break
|
||||
else:
|
||||
logger.debug("No translations for locale {locale} for plugin {name}".format(**locals()))
|
||||
|
||||
# core translations
|
||||
dirs = additional_folders + [os.path.join(ctx.app.root_path, 'translations')]
|
||||
for dirname in dirs:
|
||||
core_translations = support.Translations.load(dirname, [locale])
|
||||
if isinstance(core_translations, support.Translations):
|
||||
logger.debug("Using translation folder {dirname} for locale {locale} of core translations".format(**locals()))
|
||||
break
|
||||
else:
|
||||
translations = translations.merge(plugin_translations)
|
||||
|
||||
dirname = os.path.join(ctx.app.root_path, 'translations')
|
||||
core_translations = support.Translations.load(dirname, [locale])
|
||||
translations = translations.merge(core_translations)
|
||||
logger.debug("No core translations for locale {locale}")
|
||||
translations = translations.merge(core_translations)
|
||||
|
||||
ctx.babel_translations = translations
|
||||
return translations
|
||||
|
||||
flask.ext.babel.Babel.list_translations = fixed_list_translations
|
||||
flask.ext.babel.get_translations = fixed_get_translations
|
||||
|
||||
#~~ passive login helper
|
||||
|
|
|
|||
509
src/octoprint/server/views.py
Normal file
509
src/octoprint/server/views.py
Normal file
|
|
@ -0,0 +1,509 @@
|
|||
# coding=utf-8
|
||||
from __future__ import absolute_import
|
||||
|
||||
__author__ = "Gina Häußge <osd@foosel.net>"
|
||||
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
|
||||
__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License"
|
||||
|
||||
import os
|
||||
|
||||
from collections import defaultdict
|
||||
from flask import request, g, url_for, make_response, render_template, send_from_directory
|
||||
|
||||
import octoprint.plugin
|
||||
|
||||
from octoprint.server import app, userManager, pluginManager, gettext, debug, LOCALES, VERSION, DISPLAY_VERSION, UI_API_KEY
|
||||
from octoprint.settings import settings
|
||||
|
||||
from . import util
|
||||
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@app.route("/")
|
||||
@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, key=lambda: "view/%s/%s" % (request.path, g.locale))
|
||||
def index():
|
||||
|
||||
#~~ a bunch of settings
|
||||
|
||||
enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"])
|
||||
enable_timelapse = (settings().get(["webcam", "snapshot"]) and settings().get(["webcam", "ffmpeg"]))
|
||||
enable_systemmenu = settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0
|
||||
enable_accesscontrol = userManager is not None
|
||||
preferred_stylesheet = settings().get(["devel", "stylesheet"])
|
||||
locales = dict((l.language, dict(language=l.language, display=l.display_name, english=l.english_name)) for l in LOCALES)
|
||||
|
||||
#~~ prepare assets
|
||||
|
||||
supported_stylesheets = ("css", "less")
|
||||
assets = dict(
|
||||
js=[],
|
||||
stylesheets=[]
|
||||
)
|
||||
assets["js"] = [
|
||||
url_for('static', filename='js/app/viewmodels/appearance.js'),
|
||||
url_for('static', filename='js/app/viewmodels/connection.js'),
|
||||
url_for('static', filename='js/app/viewmodels/control.js'),
|
||||
url_for('static', filename='js/app/viewmodels/firstrun.js'),
|
||||
url_for('static', filename='js/app/viewmodels/files.js'),
|
||||
url_for('static', filename='js/app/viewmodels/loginstate.js'),
|
||||
url_for('static', filename='js/app/viewmodels/navigation.js'),
|
||||
url_for('static', filename='js/app/viewmodels/printerstate.js'),
|
||||
url_for('static', filename='js/app/viewmodels/printerprofiles.js'),
|
||||
url_for('static', filename='js/app/viewmodels/settings.js'),
|
||||
url_for('static', filename='js/app/viewmodels/slicing.js'),
|
||||
url_for('static', filename='js/app/viewmodels/temperature.js'),
|
||||
url_for('static', filename='js/app/viewmodels/terminal.js'),
|
||||
url_for('static', filename='js/app/viewmodels/users.js'),
|
||||
url_for('static', filename='js/app/viewmodels/log.js'),
|
||||
url_for('static', filename='js/app/viewmodels/usersettings.js')
|
||||
]
|
||||
if enable_gcodeviewer:
|
||||
assets["js"] += [
|
||||
url_for('static', filename='js/app/viewmodels/gcode.js'),
|
||||
url_for('static', filename='gcodeviewer/js/ui.js'),
|
||||
url_for('static', filename='gcodeviewer/js/gCodeReader.js'),
|
||||
url_for('static', filename='gcodeviewer/js/renderer.js')
|
||||
]
|
||||
if enable_timelapse:
|
||||
assets["js"].append(url_for('static', filename='js/app/viewmodels/timelapse.js'))
|
||||
|
||||
if preferred_stylesheet == "less":
|
||||
assets["stylesheets"].append(("less", url_for('static', filename='less/octoprint.less')))
|
||||
elif preferred_stylesheet == "css":
|
||||
assets["stylesheets"].append(("css", url_for('static', filename='css/octoprint.css')))
|
||||
|
||||
asset_plugins = pluginManager.get_implementations(octoprint.plugin.AssetPlugin)
|
||||
for implementation in asset_plugins:
|
||||
name = implementation._identifier
|
||||
all_assets = implementation.get_assets()
|
||||
|
||||
if "js" in all_assets:
|
||||
for asset in all_assets["js"]:
|
||||
assets["js"].append(url_for('plugin_assets', name=name, filename=asset))
|
||||
|
||||
if preferred_stylesheet in all_assets:
|
||||
for asset in all_assets[preferred_stylesheet]:
|
||||
assets["stylesheets"].append((preferred_stylesheet, url_for('plugin_assets', name=name, filename=asset)))
|
||||
else:
|
||||
for stylesheet in supported_stylesheets:
|
||||
if not stylesheet in all_assets:
|
||||
continue
|
||||
|
||||
for asset in all_assets[stylesheet]:
|
||||
assets["stylesheets"].append((stylesheet, url_for('plugin_assets', name=name, filename=asset)))
|
||||
break
|
||||
|
||||
##~~ prepare templates
|
||||
|
||||
templates = defaultdict(lambda: dict(order=[], entries=dict()))
|
||||
|
||||
# rules for transforming template configs to template entries
|
||||
template_rules = dict(
|
||||
navbar=dict(div=lambda x: "navbar_plugin_" + x, template=lambda x: x + "_navbar.jinja2", to_entry=lambda data: data),
|
||||
sidebar=dict(div=lambda x: "sidebar_plugin_" + x, template=lambda x: x + "_sidebar.jinja2", to_entry=lambda data: (data["name"], data)),
|
||||
tab=dict(div=lambda x: "tab_plugin_" + x, template=lambda x: x + "_tab.jinja2", to_entry=lambda data: (data["name"], data)),
|
||||
settings=dict(div=lambda x: "settings_plugin_" + x, template=lambda x: x + "_settings.jinja2", to_entry=lambda data: (data["name"], data)),
|
||||
usersettings=dict(div=lambda x: "usersettings_plugin_" + x, template=lambda x: x + "_usersettings.jinja2", to_entry=lambda data: (data["name"], data)),
|
||||
generic=dict(template=lambda x: x + ".jinja2", to_entry=lambda data: data)
|
||||
)
|
||||
|
||||
# sorting orders
|
||||
template_sorting = dict(
|
||||
navbar=dict(add="prepend", key=None),
|
||||
sidebar=dict(add="append", key="name"),
|
||||
tab=dict(add="append", key="name"),
|
||||
settings=dict(add="custom_append", key="name", custom_add_entries=lambda missing: dict(section_plugins=(gettext("Plugins"), None)), custom_add_order=lambda missing: ["section_plugins"] + missing),
|
||||
usersettings=dict(add="append", key="name"),
|
||||
generic=dict(add="append", key=None)
|
||||
)
|
||||
|
||||
hooks = pluginManager.get_hooks("octoprint.ui.web.templatetypes")
|
||||
for name, hook in hooks.items():
|
||||
try:
|
||||
result = hook(dict(template_rules))
|
||||
except:
|
||||
# TODO
|
||||
pass
|
||||
else:
|
||||
if not isinstance(result, list):
|
||||
continue
|
||||
|
||||
for entry in result:
|
||||
if not isinstance(entry, tuple) or not len(entry) == 3:
|
||||
continue
|
||||
|
||||
key, order, rule = entry
|
||||
|
||||
# order defaults
|
||||
if "add" not in order:
|
||||
order["add"] = "prepend"
|
||||
if "key" not in order:
|
||||
order["key"] = "name"
|
||||
|
||||
# rule defaults
|
||||
if "div" not in rule:
|
||||
# default div name: <hook plugin>_<template_key>_plugin_<plugin>
|
||||
div = "{name}_{key}_plugin_".format(**locals())
|
||||
rule["div"] = lambda x: div + x
|
||||
if "template" not in rule:
|
||||
# default template name: <plugin>_plugin_<hook plugin>_<template key>.jinja2
|
||||
template = "_plugin_{name}_{key}.jinja2".format(**locals())
|
||||
rule["template"] = lambda x: x + template
|
||||
if "to_entry" not in rule:
|
||||
# default to_entry assumes existing "name" property to be used as label for 2-tuple entry data structure (<name>, <properties>)
|
||||
rule["to_entry"] = lambda data: (data["name"], data)
|
||||
|
||||
template_rules["plugin_" + name + "_" + key] = rule
|
||||
template_sorting["plugin_" + name + "_" + key] = order
|
||||
template_types = template_rules.keys()
|
||||
|
||||
# navbar
|
||||
|
||||
templates["navbar"]["entries"] = dict(
|
||||
settings=dict(template="navbar/settings.jinja2", _div="navbar_settings", styles=["display: none"], data_bind="visible: loginState.isAdmin")
|
||||
)
|
||||
if enable_accesscontrol:
|
||||
templates["navbar"]["entries"]["login"] = dict(template="navbar/login.jinja2", _div="navbar_login", classes=["dropdown"], custom_bindings=False)
|
||||
if enable_systemmenu:
|
||||
templates["navbar"]["entries"]["systemmenu"] = dict(template="navbar/systemmenu.jinja2", _div="navbar_systemmenu", styles=["display: none"], classes=["dropdown"], data_bind="visible: loginState.isAdmin", custom_bindings=False)
|
||||
|
||||
# sidebar
|
||||
|
||||
templates["sidebar"]["entries"]= dict(
|
||||
connection=(gettext("Connection"), dict(template="sidebar/connection.jinja2", _div="connection", icon="signal", styles_wrapper=["display: none"], data_bind="visible: loginState.isAdmin")),
|
||||
state=(gettext("State"), dict(template="sidebar/state.jinja2", _div="state", icon="info-sign")),
|
||||
files=(gettext("Files"), dict(template="sidebar/files.jinja2", _div="files", icon="list", classes_content=["overflow_visible"], template_header="sidebar/files_header.jinja2"))
|
||||
)
|
||||
|
||||
# tabs
|
||||
|
||||
templates["tab"]["entries"] = dict(
|
||||
temperature=(gettext("Temperature"), dict(template="tabs/temperature.jinja2", _div="temp")),
|
||||
control=(gettext("Control"), dict(template="tabs/control.jinja2", _div="control")),
|
||||
terminal=(gettext("Terminal"), dict(template="tabs/terminal.jinja2", _div="term")),
|
||||
)
|
||||
if enable_gcodeviewer:
|
||||
templates["tab"]["entries"]["gcodeviewer"] = (gettext("GCode Viewer"), dict(template="tabs/gcodeviewer.jinja2", _div="gcode"))
|
||||
if enable_timelapse:
|
||||
templates["tab"]["entries"]["timelapse"] = (gettext("Timelapse"), dict(template="tabs/timelapse.jinja2", _div="timelapse"))
|
||||
|
||||
# settings dialog
|
||||
|
||||
templates["settings"]["entries"] = dict(
|
||||
section_printer=(gettext("Printer"), None),
|
||||
|
||||
serial=(gettext("Serial Connection"), dict(template="dialogs/settings/serialconnection.jinja2", _div="settings_serialConnection", custom_bindings=False)),
|
||||
printerprofiles=(gettext("Printer Profiles"), dict(template="dialogs/settings/printerprofiles.jinja2", _div="settings_printerProfiles", custom_bindings=False)),
|
||||
temperatures=(gettext("Temperatures"), dict(template="dialogs/settings/temperatures.jinja2", _div="settings_temperature", custom_bindings=False)),
|
||||
terminalfilters=(gettext("Terminal Filters"), dict(template="dialogs/settings/terminalfilters.jinja2", _div="settings_terminalFilters", custom_bindings=False)),
|
||||
gcodescripts=(gettext("GCODE Scripts"), dict(template="dialogs/settings/gcodescripts.jinja2", _div="settings_gcodeScripts", custom_bindings=False)),
|
||||
|
||||
section_features=(gettext("Features"), None),
|
||||
|
||||
features=(gettext("Features"), dict(template="dialogs/settings/features.jinja2", _div="settings_features", custom_bindings=False)),
|
||||
webcam=(gettext("Webcam"), dict(template="dialogs/settings/webcam.jinja2", _div="settings_webcam", custom_bindings=False)),
|
||||
api=(gettext("API"), dict(template="dialogs/settings/api.jinja2", _div="settings_api", custom_bindings=False)),
|
||||
|
||||
section_octoprint=(gettext("OctoPrint"), None),
|
||||
|
||||
folders=(gettext("Folders"), dict(template="dialogs/settings/folders.jinja2", _div="settings_folders", custom_bindings=False)),
|
||||
appearance=(gettext("Appearance"), dict(template="dialogs/settings/appearance.jinja2", _div="settings_appearance", custom_bindings=False)),
|
||||
logs=(gettext("Logs"), dict(template="dialogs/settings/logs.jinja2", _div="settings_logs")),
|
||||
)
|
||||
if enable_accesscontrol:
|
||||
templates["settings"]["entries"]["accesscontrol"] = (gettext("Access Control"), dict(template="dialogs/settings/accesscontrol.jinja2", _div="settings_users", custom_bindings=False))
|
||||
|
||||
# user settings dialog
|
||||
|
||||
if enable_accesscontrol:
|
||||
templates["usersettings"]["entries"] = dict(
|
||||
access=(gettext("Access"), dict(template="dialogs/usersettings/access.jinja2", _div="usersettings_access", custom_bindings=False)),
|
||||
interface=(gettext("Interface"), dict(template="dialogs/usersettings/interface.jinja2", _div="usersettings_interface", custom_bindings=False)),
|
||||
)
|
||||
|
||||
# extract data from template plugins
|
||||
|
||||
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)
|
||||
|
||||
plugin_vars = dict()
|
||||
plugin_names = set()
|
||||
for implementation in template_plugins:
|
||||
name = implementation._identifier
|
||||
plugin_names.add(name)
|
||||
|
||||
vars = implementation.get_template_vars()
|
||||
if not isinstance(vars, dict):
|
||||
vars = dict()
|
||||
|
||||
for var_name, var_value in vars.items():
|
||||
plugin_vars["plugin_" + name + "_" + var_name] = var_value
|
||||
|
||||
configs = implementation.get_template_configs()
|
||||
if not isinstance(configs, (list, tuple)):
|
||||
configs = []
|
||||
|
||||
includes = _process_template_configs(name, implementation, configs, template_rules)
|
||||
|
||||
for t in template_types:
|
||||
for include in includes[t]:
|
||||
if t == "navbar" or t == "generic":
|
||||
data = include
|
||||
else:
|
||||
data = include[1]
|
||||
|
||||
key = data["_key"]
|
||||
if "replaces" in data:
|
||||
key = data["replaces"]
|
||||
templates[t]["entries"][key] = include
|
||||
|
||||
#~~ order internal templates and plugins
|
||||
|
||||
# make sure that
|
||||
# 1) we only have keys in our ordered list that we have entries for and
|
||||
# 2) we have all entries located somewhere within the order
|
||||
|
||||
for t in template_types:
|
||||
default_order = settings().get(["appearance", "components", "order", t], merged=True, config=dict()) or []
|
||||
configured_order = settings().get(["appearance", "components", "order", t], merged=True) or []
|
||||
configured_disabled = settings().get(["appearance", "components", "disabled", t]) or []
|
||||
|
||||
# first create the ordered list of all component ids according to the configured order
|
||||
templates[t]["order"] = [x for x in configured_order if x in templates[t]["entries"] and not x in configured_disabled]
|
||||
|
||||
# now append the entries from the default order that are not already in there
|
||||
templates[t]["order"] += [x for x in default_order if not x in templates[t]["order"] and x in templates[t]["entries"] and not x in configured_disabled]
|
||||
|
||||
all_ordered = set(templates[t]["order"])
|
||||
all_disabled = set(configured_disabled)
|
||||
|
||||
# check if anything is missing, if not we are done here
|
||||
missing_in_order = set(templates[t]["entries"].keys()).difference(all_ordered).difference(all_disabled)
|
||||
if len(missing_in_order) == 0:
|
||||
continue
|
||||
|
||||
# finally add anything that's not included in our order yet
|
||||
sorted_missing = list(missing_in_order)
|
||||
if template_sorting[t]["key"] is not None:
|
||||
# anything but navbar and generic components get sorted by their name
|
||||
if template_sorting[t]["key"] == "name":
|
||||
sorted_missing = sorted(missing_in_order, key=lambda x: templates[t]["entries"][x][0])
|
||||
|
||||
if template_sorting[t]["add"] == "prepend":
|
||||
templates[t]["order"] = sorted_missing + templates[t]["order"]
|
||||
elif template_sorting[t]["add"] == "append":
|
||||
templates[t]["order"] += sorted_missing
|
||||
elif template_sorting[t]["add"] == "custom_prepend" and "custom_add_entries" in template_sorting[t] and "custom_add_order" in template_sorting[t]:
|
||||
templates[t]["entries"].update(template_sorting[t]["custom_add_entries"](sorted_missing))
|
||||
templates[t]["order"] = template_sorting[t]["custom_add_order"](sorted_missing) + templates[t]["order"]
|
||||
elif template_sorting[t]["add"] == "custom_append" and "custom_add_entries" in template_sorting[t] and "custom_add_order" in template_sorting[t]:
|
||||
templates[t]["entries"].update(template_sorting[t]["custom_add_entries"](sorted_missing))
|
||||
templates[t]["order"] += template_sorting[t]["custom_add_order"](sorted_missing)
|
||||
|
||||
#~~ prepare full set of template vars for rendering
|
||||
|
||||
render_kwargs = dict(
|
||||
webcamStream=settings().get(["webcam", "stream"]),
|
||||
enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
|
||||
enableAccessControl=userManager is not None,
|
||||
enableSdSupport=settings().get(["feature", "sdSupport"]),
|
||||
firstRun=settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized()),
|
||||
debug=debug,
|
||||
version=VERSION,
|
||||
display_version=DISPLAY_VERSION,
|
||||
gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]),
|
||||
gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]),
|
||||
uiApiKey=UI_API_KEY,
|
||||
templates=templates,
|
||||
assets=assets,
|
||||
pluginNames=plugin_names,
|
||||
locales=locales
|
||||
)
|
||||
render_kwargs.update(plugin_vars)
|
||||
|
||||
#~~ render!
|
||||
|
||||
return render_template(
|
||||
"index.jinja2",
|
||||
**render_kwargs
|
||||
)
|
||||
|
||||
|
||||
def _process_template_configs(name, implementation, configs, rules):
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
counters = defaultdict(lambda: 1)
|
||||
includes = defaultdict(list)
|
||||
|
||||
for config in configs:
|
||||
if not isinstance(config, dict):
|
||||
continue
|
||||
if not "type" in config:
|
||||
continue
|
||||
|
||||
template_type = config["type"]
|
||||
del config["type"]
|
||||
|
||||
if not template_type in rules:
|
||||
continue
|
||||
rule = rules[template_type]
|
||||
|
||||
data = _process_template_config(name, implementation, rule, config=config, counter=counters[template_type])
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
includes[template_type].append(rule["to_entry"](data))
|
||||
counters[template_type] += 1
|
||||
|
||||
for template_type in rules:
|
||||
if len(includes[template_type]) == 0:
|
||||
# if no template of that type was added by the config, we'll try to use the default template name
|
||||
rule = rules[template_type]
|
||||
data = _process_template_config(name, implementation, rule)
|
||||
if data is not None:
|
||||
try:
|
||||
app.jinja_env.get_or_select_template(data["template"])
|
||||
except TemplateNotFound:
|
||||
pass
|
||||
else:
|
||||
includes[template_type].append(rule["to_entry"](data))
|
||||
|
||||
return includes
|
||||
|
||||
def _process_template_config(name, implementation, rule, config=None, counter=1):
|
||||
if "mandatory" in rule:
|
||||
for mandatory in rule["mandatory"]:
|
||||
if not mandatory in config:
|
||||
return None
|
||||
|
||||
if config is None:
|
||||
config = dict()
|
||||
data = dict(config)
|
||||
|
||||
if not "suffix" in data and counter > 1:
|
||||
data["suffix"] = "_%d" % counter
|
||||
|
||||
if "div" in data:
|
||||
data["_div"] = data["div"]
|
||||
elif "div" in rule:
|
||||
data["_div"] = rule["div"](name)
|
||||
if "suffix" in data:
|
||||
data["_div"] = data["_div"] + data["suffix"]
|
||||
|
||||
if not "template" in data:
|
||||
data["template"] = rule["template"](name)
|
||||
|
||||
if not "name" in data:
|
||||
data["name"] = implementation._plugin_name
|
||||
|
||||
if not "custom_bindings" in data or data["custom_bindings"]:
|
||||
data_bind = "allowBindings: true"
|
||||
if "data_bind" in data:
|
||||
data_bind = data_bind + ", " + data["data_bind"]
|
||||
data["data_bind"] = data_bind
|
||||
|
||||
data["_key"] = "plugin_" + name
|
||||
if "suffix" in data:
|
||||
data["_key"] += data["suffix"]
|
||||
|
||||
return data
|
||||
|
||||
@app.route("/robots.txt")
|
||||
def robotsTxt():
|
||||
return send_from_directory(app.static_folder, "robots.txt")
|
||||
|
||||
|
||||
@app.route("/i18n/<string:locale>/<string:domain>.js")
|
||||
@util.flask.cached(refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values, key=lambda: "view/%s/%s" % (request.path, g.locale))
|
||||
def localeJs(locale, domain):
|
||||
messages = dict()
|
||||
plural_expr = None
|
||||
|
||||
if locale != "en":
|
||||
from flask import _request_ctx_stack
|
||||
from babel.messages.pofile import read_po
|
||||
|
||||
def messages_from_po(base_path, locale, domain):
|
||||
path = os.path.join(base_path, locale)
|
||||
if not os.path.isdir(path):
|
||||
return None, None
|
||||
|
||||
path = os.path.join(path, "LC_MESSAGES", "{domain}.po".format(**locals()))
|
||||
if not os.path.isfile(path):
|
||||
return None, None
|
||||
|
||||
messages = dict()
|
||||
with file(path) as f:
|
||||
catalog = read_po(f, locale=locale, domain=domain)
|
||||
|
||||
for message in catalog:
|
||||
message_id = message.id
|
||||
if isinstance(message_id, (list, tuple)):
|
||||
message_id = message_id[0]
|
||||
messages[message_id] = message.string
|
||||
|
||||
return messages, catalog.plural_expr
|
||||
|
||||
user_base_path = os.path.join(settings().getBaseFolder("translations"))
|
||||
user_plugin_path = os.path.join(user_base_path, "_plugins")
|
||||
|
||||
# plugin translations
|
||||
plugins = octoprint.plugin.plugin_manager().enabled_plugins
|
||||
for name, plugin in plugins.items():
|
||||
dirs = [os.path.join(user_plugin_path, name), os.path.join(plugin.location, 'translations')]
|
||||
for dirname in dirs:
|
||||
if not os.path.isdir(dirname):
|
||||
continue
|
||||
|
||||
plugin_messages, _ = messages_from_po(dirname, locale, domain)
|
||||
|
||||
if plugin_messages is not None:
|
||||
messages = octoprint.util.dict_merge(messages, plugin_messages)
|
||||
_logger.debug("Using translation folder {dirname} for locale {locale} of plugin {name}".format(**locals()))
|
||||
break
|
||||
else:
|
||||
_logger.debug("No translations for locale {locale} for plugin {name}".format(**locals()))
|
||||
|
||||
# core translations
|
||||
ctx = _request_ctx_stack.top
|
||||
base_path = os.path.join(ctx.app.root_path, "translations")
|
||||
|
||||
dirs = [user_base_path, base_path]
|
||||
for dirname in dirs:
|
||||
core_messages, plural_expr = messages_from_po(dirname, locale, domain)
|
||||
|
||||
if core_messages is not None:
|
||||
messages = octoprint.util.dict_merge(messages, core_messages)
|
||||
_logger.debug("Using translation folder {dirname} for locale {locale} of core translations".format(**locals()))
|
||||
break
|
||||
else:
|
||||
_logger.debug("No core translations for locale {locale}".format(**locals()))
|
||||
|
||||
catalog = dict(
|
||||
messages=messages,
|
||||
plural_expr=plural_expr,
|
||||
locale=locale,
|
||||
domain=domain
|
||||
)
|
||||
|
||||
return render_template("i18n.js.jinja2", catalog=catalog)
|
||||
|
||||
|
||||
@app.route("/plugin_assets/<string:name>/<path:filename>")
|
||||
def plugin_assets(name, filename):
|
||||
asset_plugins = pluginManager.get_filtered_implementations(lambda p: p._identifier == name, octoprint.plugin.AssetPlugin)
|
||||
|
||||
if not asset_plugins:
|
||||
return make_response("Asset not found", 404)
|
||||
|
||||
if len(asset_plugins) > 1:
|
||||
return make_response("More than one asset provider for {name}, can't proceed".format(name=name), 500)
|
||||
|
||||
asset_plugin = asset_plugins[0]
|
||||
asset_folder = asset_plugin.get_asset_folder()
|
||||
if asset_folder is None:
|
||||
return make_response("Asset not found", 404)
|
||||
|
||||
return send_from_directory(asset_folder, filename)
|
||||
|
||||
|
||||
|
|
@ -153,7 +153,8 @@ default_settings = {
|
|||
"plugins": None,
|
||||
"slicingProfiles": None,
|
||||
"printerProfiles": None,
|
||||
"scripts": None
|
||||
"scripts": None,
|
||||
"translations": None
|
||||
},
|
||||
"temperature": {
|
||||
"profiles": [
|
||||
|
|
|
|||
|
|
@ -16,6 +16,37 @@ $(function() {
|
|||
self.appearance_defaultLanguage = ko.observable();
|
||||
|
||||
self.settingsDialog = undefined;
|
||||
self.translationManagerDialog = undefined;
|
||||
self.translationUploadElement = $("#settings_appearance_managelanguagesdialog_upload");
|
||||
self.translationUploadButton = $("#settings_appearance_managelanguagesdialog_upload_start");
|
||||
|
||||
self.translationUploadFilename = ko.observable();
|
||||
self.invalidTranslationArchive = ko.computed(function() {
|
||||
var name = self.translationUploadFilename();
|
||||
return name !== undefined && !(_.endsWith(name.toLocaleLowerCase(), ".zip") || _.endsWith(name.toLocaleLowerCase(), ".tar.gz") || _.endsWith(name.toLocaleLowerCase(), ".tgz") || _.endsWith(name.toLocaleLowerCase(), ".tar"));
|
||||
});
|
||||
self.enableTranslationUpload = ko.computed(function() {
|
||||
var name = self.translationUploadFilename();
|
||||
return name !== undefined && name.trim() != "" && !self.invalidTranslationArchive();
|
||||
});
|
||||
|
||||
self.translations = new ItemListHelper(
|
||||
"settings.translations",
|
||||
{
|
||||
"locale": function (a, b) {
|
||||
// sorts ascending
|
||||
if (a["locale"].toLocaleLowerCase() < b["locale"].toLocaleLowerCase()) return -1;
|
||||
if (a["locale"].toLocaleLowerCase() > b["locale"].toLocaleLowerCase()) return 1;
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
{
|
||||
},
|
||||
"locale",
|
||||
[],
|
||||
[],
|
||||
0
|
||||
);
|
||||
|
||||
self.appearance_available_colors = ko.observable([
|
||||
{key: "default", name: gettext("default")},
|
||||
|
|
@ -108,7 +139,7 @@ $(function() {
|
|||
self.scripts_gcode_afterPrintPaused = ko.observable(undefined);
|
||||
self.scripts_gcode_beforePrintResumed = ko.observable(undefined);
|
||||
self.scripts_gcode_afterPrinterConnected = ko.observable(undefined);
|
||||
|
||||
|
||||
self.temperature_profiles = ko.observableArray(undefined);
|
||||
self.temperature_cutoff = ko.observable(undefined);
|
||||
|
||||
|
|
@ -140,11 +171,43 @@ $(function() {
|
|||
|
||||
self.onStartup = function() {
|
||||
self.settingsDialog = $('#settings_dialog');
|
||||
self.translationManagerDialog = $('#settings_appearance_managelanguagesdialog');
|
||||
self.translationUploadElement = $("#settings_appearance_managelanguagesdialog_upload");
|
||||
self.translationUploadButton = $("#settings_appearance_managelanguagesdialog_upload_start");
|
||||
|
||||
self.translationUploadElement.fileupload({
|
||||
dataType: "json",
|
||||
maxNumberOfFiles: 1,
|
||||
autoUpload: false,
|
||||
add: function(e, data) {
|
||||
if (data.files.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.translationUploadFilename(data.files[0].name);
|
||||
|
||||
self.translationUploadButton.unbind("click");
|
||||
self.translationUploadButton.bind("click", function() {
|
||||
data.submit();
|
||||
return false;
|
||||
});
|
||||
},
|
||||
done: function(e, data) {
|
||||
self.translationUploadButton.unbind("click");
|
||||
self.translationUploadFilename(undefined);
|
||||
self.fromTranslationResponse(data.result);
|
||||
},
|
||||
fail: function(e, data) {
|
||||
self.translationUploadButton.unbind("click");
|
||||
self.translationUploadFilename(undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
self.onAllBound = function(allViewModels) {
|
||||
self.settingsDialog.on('show', function(event) {
|
||||
if (event.target.id == "settings_dialog") {
|
||||
self.requestTranslationData();
|
||||
_.each(allViewModels, function(viewModel) {
|
||||
if (viewModel.hasOwnProperty("onSettingsShown")) {
|
||||
viewModel.onSettingsShown();
|
||||
|
|
@ -182,6 +245,11 @@ $(function() {
|
|||
return false;
|
||||
};
|
||||
|
||||
self.showTranslationManager = function() {
|
||||
self.translationManagerDialog.modal();
|
||||
return false;
|
||||
};
|
||||
|
||||
self.requestData = function(callback) {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "settings",
|
||||
|
|
@ -194,6 +262,71 @@ $(function() {
|
|||
});
|
||||
};
|
||||
|
||||
self.requestTranslationData = function(callback) {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "languages",
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function(response) {
|
||||
self.fromTranslationResponse(response);
|
||||
if (callback) callback();
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
self.fromTranslationResponse = function(response) {
|
||||
var translationsByLocale = {};
|
||||
_.each(response.language_packs, function(item, key) {
|
||||
_.each(item.languages, function(pack) {
|
||||
var locale = pack.locale;
|
||||
if (!_.has(translationsByLocale, locale)) {
|
||||
translationsByLocale[locale] = {
|
||||
locale: locale,
|
||||
display: pack.locale_display,
|
||||
english: pack.locale_english,
|
||||
packs: []
|
||||
};
|
||||
}
|
||||
|
||||
translationsByLocale[locale]["packs"].push({
|
||||
identifier: key,
|
||||
display: item.display,
|
||||
pack: pack
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var translations = [];
|
||||
_.each(translationsByLocale, function(item) {
|
||||
item["packs"].sort(function(a, b) {
|
||||
if (a.identifier == "_core") return -1;
|
||||
if (b.identifier == "_core") return 1;
|
||||
|
||||
if (a.display < b.display) return -1;
|
||||
if (a.display > b.display) return 1;
|
||||
return 0;
|
||||
});
|
||||
translations.push(item);
|
||||
});
|
||||
|
||||
self.translations.updateItems(translations);
|
||||
};
|
||||
|
||||
self.languagePackDisplay = function(item) {
|
||||
return item.display + ((item.english != undefined) ? ' (' + item.english + ')' : '');
|
||||
};
|
||||
|
||||
self.deleteLanguagePack = function(locale, pack) {
|
||||
$.ajax({
|
||||
url: API_BASEURL + "languages/" + locale + "/" + pack,
|
||||
type: "DELETE",
|
||||
dataType: "json",
|
||||
success: function(response) {
|
||||
self.fromTranslationResponse(response);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
self.fromResponse = function(response) {
|
||||
if (self.settings === undefined) {
|
||||
self.settings = ko.mapping.fromJS(response);
|
||||
|
|
@ -263,7 +396,7 @@ $(function() {
|
|||
self.scripts_gcode_afterPrintPaused(response.scripts.gcode.afterPrintPaused);
|
||||
self.scripts_gcode_beforePrintResumed(response.scripts.gcode.beforePrintResumed);
|
||||
self.scripts_gcode_afterPrinterConnected(response.scripts.gcode.afterPrinterConnected);
|
||||
|
||||
|
||||
self.temperature_profiles(response.temperature.profiles);
|
||||
self.temperature_cutoff(response.temperature.cutoff);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" title="">
|
||||
<label class="control-label" for="settings-appearanceLanguages">{{ _('Language Packs') }}</label>
|
||||
<div class="controls">
|
||||
<button class="btn" data-bind="click: showTranslationManager">{{ _('Manage...') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group" title="">
|
||||
<label class="control-label" for="settings-appearanceDefaultLanguage">{{ _('Default Language') }}</label>
|
||||
<div class="controls">
|
||||
|
|
@ -31,3 +37,48 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="settings_appearance_managelanguagesdialog" class="modal hide fade">
|
||||
<div class="modal-header">
|
||||
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">×</a>
|
||||
<h3>{{ _('Manage Language Packs...') }}</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="settings_appearance_managelanguagesdialog_list" data-bind="slimScrolledForeach: translations.paginatedItems" style="height: 300px">
|
||||
<div class="entry">
|
||||
<strong><a href="#" onclick="$(this).children('i').toggleClass('icon-caret-right icon-caret-down').parent().parent().next().slideToggle('fast')"><i class="icon-caret-down"></i> <span data-bind="text: $root.languagePackDisplay($data)"></span></a></strong>
|
||||
<div class="packs">
|
||||
<!-- ko foreach: $data.packs -->
|
||||
<div class="row-fluid">
|
||||
<div class="span8 offset1">
|
||||
<strong data-bind="text: display"></strong><br />
|
||||
<small data-bind="visible: pack.last_update" class="muted">{{ _('Last update:') }} <span data-bind="text: formatDate($data.pack.last_update)"></span></small>
|
||||
</div>
|
||||
<div class="span3">
|
||||
<button class="btn btn-block btn-small" data-bind="click: function() {$root.deleteLanguagePack($data.pack.locale, $data.identifier)}"><i class="icon-trash"></i> {{ _('Delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<form class="form-inline">
|
||||
<div class="control-group row-fluid" data-bind="css: {error: invalidTranslationArchive}">
|
||||
<div class="input-prepend span9">
|
||||
<span class="btn fileinput-button">
|
||||
<span>{{ _('Browse...') }}</span>
|
||||
<input id="settings_appearance_managelanguagesdialog_upload" type="file" name="file" data-url="{{ url_for("api.uploadLanguagePack") }}">
|
||||
</span>
|
||||
<span class="add-on add-on-limited text-left" data-bind="text: translationUploadFilename, attr: {title: translationUploadFilename}"></span>
|
||||
</div>
|
||||
<button id="settings_appearance_managelanguagesdialog_upload_start" class="btn btn-primary span3" data-bind="enable: enableTranslationUpload, css: {disabled: !enableTranslationUpload()}, click: function(){}">{{ _('Upload') }}</button>
|
||||
</div>
|
||||
<span class="help-block" data-bind="visible: invalidTranslationArchive">{{ _('This does not look like a valid language pack. Valid language packs should be either zip files or tarballs and have the extension ".zip", ".tar.gz", ".tgz" or ".tar"') }}</span>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -292,14 +292,149 @@ class CompileTranslation(Command):
|
|||
self.babel_compile_messages.run()
|
||||
|
||||
|
||||
def get_babel_commandclasses(pot_file=None, mapping_file="babel.cfg", input_dirs=".", output_dir=None, mail_address="i18n@octoprint.org", copyright_holder="The OctoPrint Project"):
|
||||
return dict(
|
||||
class BundleTranslation(Command):
|
||||
description = "bundles translations"
|
||||
user_options = [
|
||||
('locale=', 'l', 'locale for the translation to bundle')
|
||||
]
|
||||
boolean_options = []
|
||||
|
||||
source_dir = None
|
||||
target_dir = None
|
||||
|
||||
@classmethod
|
||||
def for_options(cls, source_dir=None, target_dir=None):
|
||||
if source_dir is None:
|
||||
raise ValueError("source_dir must not be None")
|
||||
if target_dir is None:
|
||||
raise ValueError("target_dir must not be None")
|
||||
|
||||
return type(cls)(cls.__name__, (cls,), dict(
|
||||
source_dir=source_dir,
|
||||
target_dir=target_dir
|
||||
))
|
||||
|
||||
def initialize_options(self):
|
||||
self.locale = None
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
locale = self.locale
|
||||
source_path = os.path.join(self.__class__.source_dir, locale)
|
||||
target_path = os.path.join(self.__class__.target_dir, locale)
|
||||
|
||||
if not os.path.exists(source_path):
|
||||
raise RuntimeError("source path " + source_path + " does not exist")
|
||||
|
||||
if os.path.exists(target_path):
|
||||
if not os.path.isdir(target_path):
|
||||
raise RuntimeError("target path " + target_path + " exists and is not a directory")
|
||||
shutil.rmtree(target_path)
|
||||
|
||||
print("Copying translations for locale {locale} from {source_path} to {target_path}...".format(**locals()))
|
||||
shutil.copytree(source_path, target_path)
|
||||
|
||||
|
||||
class PackTranslation(Command):
|
||||
description = "bundles translations"
|
||||
user_options = [
|
||||
('locale=', 'l', 'locale for the translation to bundle'),
|
||||
('author=', 'a', 'author of the translation')
|
||||
]
|
||||
boolean_options = []
|
||||
|
||||
source_dir = None
|
||||
pack_name_prefix = None
|
||||
pack_path_prefix = None
|
||||
|
||||
@classmethod
|
||||
def for_options(cls, source_dir=None, pack_name_prefix=None, pack_path_prefix=None):
|
||||
if source_dir is None:
|
||||
raise ValueError("source_dir must not be None")
|
||||
if pack_name_prefix is None:
|
||||
raise ValueError("pack_name_prefix must not be None")
|
||||
if pack_path_prefix is None:
|
||||
raise ValueError("pack_path_prefix must not be None")
|
||||
|
||||
return type(cls)(cls.__name__, (cls,), dict(
|
||||
source_dir=source_dir,
|
||||
pack_name_prefix=pack_name_prefix,
|
||||
pack_path_prefix=pack_path_prefix
|
||||
))
|
||||
|
||||
def initialize_options(self):
|
||||
self.locale = None
|
||||
self.author = None
|
||||
|
||||
def finalize_options(self):
|
||||
if self.locale is None:
|
||||
raise ValueError("locale must be provided")
|
||||
|
||||
def run(self):
|
||||
locale = self.locale
|
||||
locale_dir = os.path.join(self.__class__.source_dir, locale)
|
||||
|
||||
if not os.path.isdir(locale_dir):
|
||||
raise RuntimeError("translation does not exist, please create it first")
|
||||
|
||||
import datetime
|
||||
|
||||
now = datetime.datetime.utcnow().replace(microsecond=0)
|
||||
|
||||
zip_path = os.path.join(self.__class__.source_dir, "{prefix}{locale}_{date}.zip".format(prefix=self.__class__.pack_name_prefix, locale=locale, date=now.strftime("%Y%m%d%H%M%S")))
|
||||
print("Packing translation to {zip_path}".format(**locals()))
|
||||
|
||||
def add_recursively(zip, path, prefix):
|
||||
if not os.path.isdir(path):
|
||||
return
|
||||
|
||||
for entry in os.listdir(path):
|
||||
entry_path = os.path.join(path, entry)
|
||||
new_prefix = prefix + "/" + entry
|
||||
if os.path.isdir(entry_path):
|
||||
add_recursively(zip, entry_path, new_prefix)
|
||||
elif os.path.isfile(entry_path):
|
||||
print("Adding {entry_path} as {new_prefix}".format(**locals()))
|
||||
zip.write(entry_path, new_prefix)
|
||||
|
||||
meta_str = "last_update: {date}\n".format(date=now.isoformat())
|
||||
if self.author:
|
||||
meta_str += "author: {author}\n".format(author=self.author)
|
||||
|
||||
zip_locale_root = self.__class__.pack_path_prefix + locale
|
||||
|
||||
import zipfile
|
||||
with zipfile.ZipFile(zip_path, "w") as zip:
|
||||
add_recursively(zip, locale_dir, zip_locale_root)
|
||||
|
||||
print("Adding meta.yaml as {zip_locale_root}/meta.yaml".format(**locals()))
|
||||
zip.writestr(zip_locale_root + "/meta.yaml", meta_str)
|
||||
|
||||
|
||||
def get_babel_commandclasses(pot_file=None,
|
||||
mapping_file="babel.cfg",
|
||||
input_dirs=".",
|
||||
output_dir=None,
|
||||
pack_name_prefix=None,
|
||||
pack_path_prefix=None,
|
||||
bundled_dir=None,
|
||||
mail_address="i18n@octoprint.org",
|
||||
copyright_holder="The OctoPrint Project"):
|
||||
result = dict(
|
||||
babel_new=NewTranslation.for_options(pot_file=pot_file, output_dir=output_dir),
|
||||
babel_extract=ExtractTranslation.for_options(mapping_file=mapping_file, pot_file=pot_file, input_dirs=input_dirs, mail_address=mail_address, copyright_holder=copyright_holder),
|
||||
babel_refresh=RefreshTranslation.for_options(mapping_file=mapping_file, pot_file=pot_file, input_dirs=input_dirs, output_dir=output_dir, mail_address=mail_address, copyright_holder=copyright_holder),
|
||||
babel_compile=CompileTranslation.for_options(output_dir=output_dir)
|
||||
babel_compile=CompileTranslation.for_options(output_dir=output_dir),
|
||||
babel_pack=PackTranslation.for_options(source_dir=output_dir, pack_name_prefix=pack_name_prefix, pack_path_prefix=pack_path_prefix)
|
||||
)
|
||||
|
||||
if bundled_dir is not None:
|
||||
result["babel_bundle"] = BundleTranslation.for_options(source_dir=output_dir, target_dir=bundled_dir)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def create_plugin_setup_parameters(identifier="todo", name="TODO", version="0.1", description="TODO", author="TODO",
|
||||
mail="todo@example.com", url="TODO", license="AGPLv3", additional_data=None,
|
||||
|
|
@ -341,10 +476,11 @@ def create_plugin_setup_parameters(identifier="todo", name="TODO", version="0.1"
|
|||
clean=CleanCommand.for_options(source_folder=package, eggs=eggs)
|
||||
))
|
||||
|
||||
translation_dir = os.path.join(package, "translations")
|
||||
translation_dir = os.path.join("translations")
|
||||
pot_file = os.path.join(translation_dir, "messages.pot")
|
||||
bundled_dir = os.path.join(package, "translations")
|
||||
if os.path.isdir(translation_dir) and os.path.isfile(pot_file):
|
||||
cmdclass.update(get_babel_commandclasses(pot_file=pot_file, output_dir=translation_dir))
|
||||
cmdclass.update(get_babel_commandclasses(pot_file=pot_file, output_dir=translation_dir, bundled_dir=bundled_dir, pack_name_prefix="{name}-i18n-".format(**locals()), pack_path_prefix="_plugins/{identifier}/".format(**locals())))
|
||||
|
||||
return dict(
|
||||
name=name,
|
||||
|
|
|
|||
BIN
translations/de/LC_MESSAGES/messages.mo
Normal file
BIN
translations/de/LC_MESSAGES/messages.mo
Normal file
Binary file not shown.
2186
translations/de/LC_MESSAGES/messages.po
Normal file
2186
translations/de/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -6,9 +6,9 @@
|
|||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: OctoPrint 1.2.0-dev-880-gff9765f-dirty\n"
|
||||
"Project-Id-Version: OctoPrint 1.2.0-dev-884-g449dd43-dirty\n"
|
||||
"Report-Msgid-Bugs-To: i18n@octoprint.org\n"
|
||||
"POT-Creation-Date: 2015-05-29 17:21+0200\n"
|
||||
"POT-Creation-Date: 2015-06-02 13:09+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -98,6 +98,7 @@ msgstr ""
|
|||
|
||||
#: src/octoprint/plugins/cura/templates/cura_settings.jinja2:68
|
||||
#: src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2:132
|
||||
#: src/octoprint/templates/dialogs/settings/appearance.jinja2:71
|
||||
msgid "Browse..."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -385,6 +386,7 @@ msgstr ""
|
|||
|
||||
#: src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2:51
|
||||
#: src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2:143
|
||||
#: src/octoprint/templates/dialogs/settings/appearance.jinja2:82
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -465,77 +467,77 @@ msgid ""
|
|||
"\".tar.gz\", \".tgz\" or \".tar\""
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:224
|
||||
#: src/octoprint/server/views.py:123
|
||||
msgid "Connection"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:225
|
||||
#: src/octoprint/server/views.py:124
|
||||
msgid "State"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:226
|
||||
#: src/octoprint/server/views.py:125
|
||||
msgid "Files"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:232
|
||||
#: src/octoprint/server/views.py:131
|
||||
msgid "Temperature"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:233
|
||||
#: src/octoprint/server/views.py:132
|
||||
msgid "Control"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:234
|
||||
#: src/octoprint/server/views.py:133
|
||||
msgid "Terminal"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:237
|
||||
#: src/octoprint/server/views.py:136
|
||||
msgid "GCode Viewer"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:239
|
||||
#: src/octoprint/server/views.py:138
|
||||
#: src/octoprint/templates/sidebar/state.jinja2:3
|
||||
msgid "Timelapse"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:244
|
||||
#: src/octoprint/server/views.py:143
|
||||
msgid "Printer"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:246
|
||||
#: src/octoprint/server/views.py:145
|
||||
msgid "Serial Connection"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:247
|
||||
#: src/octoprint/server/views.py:146
|
||||
#: src/octoprint/templates/dialogs/settings/printerprofiles.jinja2:1
|
||||
msgid "Printer Profiles"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:248
|
||||
#: src/octoprint/server/views.py:147
|
||||
msgid "Temperatures"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:249
|
||||
#: src/octoprint/server/views.py:148
|
||||
msgid "Terminal Filters"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:250
|
||||
#: src/octoprint/server/views.py:149
|
||||
msgid "GCODE Scripts"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:252 src/octoprint/server/__init__.py:254
|
||||
#: src/octoprint/server/views.py:151 src/octoprint/server/views.py:153
|
||||
msgid "Features"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:255
|
||||
#: src/octoprint/server/views.py:154
|
||||
msgid "Webcam"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:256
|
||||
#: src/octoprint/server/views.py:155
|
||||
msgid "API"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:258
|
||||
#: src/octoprint/server/views.py:157
|
||||
#: src/octoprint/static/js/app/viewmodels/appearance.js:11
|
||||
#: src/octoprint/static/js/app/viewmodels/appearance.js:13
|
||||
#: src/octoprint/static/js/app/viewmodels/appearance.js:18
|
||||
|
|
@ -543,32 +545,32 @@ msgstr ""
|
|||
msgid "OctoPrint"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:260
|
||||
#: src/octoprint/server/views.py:159
|
||||
msgid "Folders"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:261
|
||||
#: src/octoprint/server/views.py:160
|
||||
msgid "Appearance"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:262
|
||||
#: src/octoprint/server/views.py:161
|
||||
#: src/octoprint/templates/dialogs/settings/logs.jinja2:2
|
||||
msgid "Logs"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:265
|
||||
#: src/octoprint/server/views.py:164
|
||||
msgid "Access Control"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:271
|
||||
#: src/octoprint/server/views.py:170
|
||||
msgid "Access"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:272
|
||||
#: src/octoprint/server/views.py:171
|
||||
msgid "Interface"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/server/__init__.py:359
|
||||
#: src/octoprint/server/views.py:258
|
||||
msgid "Plugins"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -890,44 +892,44 @@ msgid "Error"
|
|||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/printerprofiles.js:96
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:21
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:51
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:52
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:82
|
||||
msgid "default"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/printerprofiles.js:97
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:22
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:35
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:53
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:66
|
||||
msgid "red"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/printerprofiles.js:98
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:23
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:37
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:54
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:68
|
||||
msgid "orange"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/printerprofiles.js:99
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:24
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:39
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:55
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:70
|
||||
msgid "yellow"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/printerprofiles.js:100
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:25
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:41
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:56
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:72
|
||||
msgid "green"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/printerprofiles.js:101
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:26
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:43
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:57
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:74
|
||||
msgid "blue"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/printerprofiles.js:102
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:28
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:47
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:59
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:78
|
||||
msgid "black"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1033,17 +1035,17 @@ msgstr ""
|
|||
msgid "This will restart the print job from the beginning."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:27
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:45
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:58
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:76
|
||||
msgid "violet"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:29
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:49
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:60
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:80
|
||||
msgid "white"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:57
|
||||
#: src/octoprint/static/js/app/viewmodels/settings.js:88
|
||||
msgid "Autodetect from browser"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1398,16 +1400,46 @@ msgid "Transparent Color"
|
|||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/dialogs/settings/appearance.jinja2:23
|
||||
msgid "Language Packs"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/dialogs/settings/appearance.jinja2:25
|
||||
msgid "Manage..."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/dialogs/settings/appearance.jinja2:29
|
||||
msgid "Default Language"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/dialogs/settings/appearance.jinja2:30
|
||||
#: src/octoprint/templates/dialogs/settings/appearance.jinja2:36
|
||||
msgid ""
|
||||
"Changes to the default interface language will only become active after a"
|
||||
" reload of the page and only be active if not overridden by the users "
|
||||
"language settings."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/dialogs/settings/appearance.jinja2:44
|
||||
msgid "Manage Language Packs..."
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/dialogs/settings/appearance.jinja2:58
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/dialogs/settings/appearance.jinja2:76
|
||||
#: src/octoprint/templates/overlays/dragndrop.jinja2:10
|
||||
#: src/octoprint/templates/sidebar/files.jinja2:44
|
||||
#: src/octoprint/templates/sidebar/files.jinja2:55
|
||||
msgid "Upload"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/dialogs/settings/appearance.jinja2:78
|
||||
msgid ""
|
||||
"This does not look like a valid language pack. Valid language packs "
|
||||
"should be either zip files or tarballs and have the extension \".zip\", "
|
||||
"\".tar.gz\", \".tgz\" or \".tar\""
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/dialogs/settings/features.jinja2:5
|
||||
msgid "Enable Temperature Graph"
|
||||
msgstr ""
|
||||
|
|
@ -1836,12 +1868,6 @@ msgstr ""
|
|||
msgid "SD not initialized"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/overlays/dragndrop.jinja2:10
|
||||
#: src/octoprint/templates/sidebar/files.jinja2:44
|
||||
#: src/octoprint/templates/sidebar/files.jinja2:55
|
||||
msgid "Upload"
|
||||
msgstr ""
|
||||
|
||||
#: src/octoprint/templates/overlays/offline.jinja2:9
|
||||
msgid "Attempt to reconnect"
|
||||
msgstr ""
|
||||
Loading…
Reference in a new issue