Merge branch 'dev/translationsAsPackages' into devel

Conflicts:
	src/octoprint/server/__init__.py
This commit is contained in:
Gina Häußge 2015-06-02 13:58:08 +02:00
commit 02b7085543
23 changed files with 3423 additions and 7642 deletions

View file

@ -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

View file

@ -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")

View file

@ -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"

View 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

View file

@ -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

View 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)

View file

@ -153,7 +153,8 @@ default_settings = {
"plugins": None,
"slicingProfiles": None,
"printerProfiles": None,
"scripts": None
"scripts": None,
"translations": None
},
"temperature": {
"profiles": [

View file

@ -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);

View file

@ -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">&times;</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>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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,

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -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 ""