First version of OctoPrint with i18n/l10n support

Also included is a translation for (informal) german.

New languages can be added with "python setup.py babel_new --locale=<language code>" which will create the corresponding .po file from the existing .pot file under "src/octoprint/translations/<language code>". Translations can be refreshed from strings in source with "python setup.py babel_refresh". Existing translations can be compiled into usable translation files (.mo for python and .js for Javascript) via "python setup.py babel_compile".

 You'll need to install the development dependencies for all of this to work, just issue "pip install -r requirements-dev.txt"

 Note: numbers are not yet correctly formatted for their respective locale (e.g. "2.5mm" instead of "2,5mm" in german).
This commit is contained in:
Gina Häußge 2014-08-27 14:46:46 +02:00
parent b78bb9e970
commit 53e52c841b
30 changed files with 3258 additions and 343 deletions

6
babel.cfg Normal file
View file

@ -0,0 +1,6 @@
[python: src/octoprint/**.py]
[jinja2: src/octoprint/templates/**.jinja2]
extensions=jinja2.ext.autoescape, jinja2.ext.with_
[javascript: src/octoprint/static/js/app/**.js]
extract_messages = gettext, ngettext

View file

@ -2,3 +2,4 @@ mock>=1.0.1
nose>=1.3.0
sphinxcontrib-httpdomain
sphinx_rtd_theme
po2json

View file

@ -5,6 +5,7 @@ sockjs-tornado>=1.0.0
PyYAML==3.10
Flask-Login==0.2.2
Flask-Principal==0.3.5
Flask-Babel==0.9
pyserial
netaddr
watchdog

101
setup.py
View file

@ -13,6 +13,15 @@ import os
import shutil
import glob
from babel.messages import frontend as babel
import po2json
I18N_MAPPING_FILE = "babel.cfg"
I18N_DOMAIN = "messages"
I18N_INPUT_DIRS = "."
I18N_OUTPUT_DIR_PY = os.path.join("src", "octoprint", "translations")
I18N_OUTPUT_DIR_JS = os.path.join("src", "octoprint", "static", "js", "i18n")
I18N_POT_FILE = os.path.join(I18N_OUTPUT_DIR_PY, "messages.pot")
def package_data_dirs(source, sub_folders):
dirs = []
@ -47,10 +56,100 @@ class CleanCommand(Command):
shutil.rmtree(egg)
class NewTranslation(Command):
description = "create a new translation"
user_options = [
('locale=', 'l', 'locale for the new translation'),
]
boolean_options = []
def __init__(self, dist, **kw):
self.babel_init_messages = babel.init_catalog(dist)
Command.__init__(self, dist, **kw)
def initialize_options(self):
self.locale = None
self.babel_init_messages.initialize_options()
def finalize_options(self):
self.babel_init_messages.locale = self.locale
self.babel_init_messages.input_file = I18N_POT_FILE
self.babel_init_messages.output_dir = I18N_OUTPUT_DIR_PY
self.babel_init_messages.finalize_options()
def run(self):
self.babel_init_messages.run()
class RefreshTranslation(Command):
description = "refresh translations"
user_options = []
boolean_options = []
def __init__(self, dist, **kw):
self.babel_extract_messages = babel.extract_messages(dist)
self.babel_update_messages = babel.update_catalog(dist)
Command.__init__(self, dist, **kw)
def initialize_options(self):
self.babel_extract_messages.initialize_options()
self.babel_update_messages.initialize_options()
def finalize_options(self):
self.babel_extract_messages.mapping_file = I18N_MAPPING_FILE
self.babel_extract_messages.output_file = I18N_POT_FILE
self.babel_extract_messages.input_dirs = I18N_INPUT_DIRS
self.babel_extract_messages.msgid_bugs_address = "i18n@octoprint.org"
self.babel_extract_messages.copyright_holder = "The OctoPrint Project"
self.babel_extract_messages.finalize_options()
self.babel_update_messages.input_file = I18N_MAPPING_FILE
self.babel_update_messages.output_dir = I18N_OUTPUT_DIR_PY
def run(self):
self.babel_extract_messages.run()
self.babel_update_messages.run()
class CompileTranslation(Command):
description = "compile translations"
user_options = []
boolean_options = []
def __init__(self, dist, **kw):
self.babel_compile_messages = babel.compile_catalog(dist)
Command.__init__(self, dist, **kw)
def initialize_options(self):
self.babel_compile_messages.initialize_options()
def finalize_options(self):
self.babel_compile_messages.directory = I18N_OUTPUT_DIR_PY
def run(self):
self.babel_compile_messages.run()
for lang_code in os.listdir(I18N_OUTPUT_DIR_PY):
full_path = os.path.join(I18N_OUTPUT_DIR_PY, lang_code)
if os.path.isdir(full_path):
client_po_dir = os.path.join(full_path, "LC_MESSAGES")
po2json.update_js_file(
"%s/%s.po" % (client_po_dir, I18N_DOMAIN),
lang_code,
I18N_OUTPUT_DIR_JS,
I18N_DOMAIN
)
def get_cmdclass():
cmdclass = versioneer.get_cmdclass()
cmdclass.update({
'clean': CleanCommand
'clean': CleanCommand,
'babel_new': NewTranslation,
'babel_refresh': RefreshTranslation,
'babel_compile': CompileTranslation
})
return cmdclass

View file

@ -7,9 +7,11 @@ __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
from flask import Flask, render_template, send_from_directory, g, request
from flask.ext.login import LoginManager
from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed
from flask.ext.babel import Babel
from babel import Locale
from watchdog.observers import Observer
import os
@ -20,6 +22,7 @@ SUCCESS = {}
NO_CONTENT = ("", 204)
app = Flask("octoprint")
babel = Babel(app)
debug = False
printer = None
@ -49,6 +52,35 @@ UI_API_KEY = ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes)
VERSION = octoprint._version.get_versions()['version']
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()
@babel.localeselector
def get_locale():
if "l10n" in request.values:
return Locale.negotiate([request.values["l10n"]], LANGUAGES)
return request.accept_languages.best_match(LANGUAGES)
@app.route("/")
def index():
return render_template(

View file

@ -40,11 +40,7 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
};
self._onclose = function() {
$("#offline_overlay_message").html(
"The server appears to be offline, at least I'm not getting any response from it. I'll try to reconnect " +
"automatically <strong>over the next couple of minutes</strong>, however you are welcome to try a manual reconnect " +
"anytime using the button below."
);
$("#offline_overlay_message").html(gettext("The server appears to be offline, at least I'm not getting any response from it. I'll try to reconnect automatically <strong>over the next couple of minutes</strong>, however you are welcome to try a manual reconnect anytime using the button below."));
if (!$("#offline_overlay").is(":visible"))
$("#offline_overlay").show();
@ -59,10 +55,7 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
};
self._onreconnectfailed = function() {
$("#offline_overlay_message").html(
"The server appears to be offline, at least I'm not getting any response from it. I <strong>could not reconnect automatically</strong>, " +
"but you may try a manual reconnect using the button below."
);
$("#offline_overlay_message").html(gettext("The server appears to be offline, at least I'm not getting any response from it. I <strong>could not reconnect automatically</strong>, but you may try a manual reconnect using the button below."));
};
self._onmessage = function(e) {
@ -129,40 +122,40 @@ function DataUpdater(loginStateViewModel, connectionViewModel, printerStateViewM
if ((type == "UpdatedFiles" && payload.type == "gcode") || type == "MetadataAnalysisFinished") {
gcodeFilesViewModel.requestData();
} else if (type == "MovieRendering") {
new PNotify({title: "Rendering timelapse", text: "Now rendering timelapse " + payload.movie_basename});
new PNotify({title: gettext("Rendering timelapse"), text: _.sprintf(gettext("Now rendering timelapse %(movie_basename)s"), payload)});
} else if (type == "MovieDone") {
new PNotify({title: "Timelapse ready", text: "New timelapse " + payload.movie_basename + " is done rendering."});
new PNotify({title: gettext("Timelapse ready"), text: _.sprintf(gettext("New timelapse %(movie_basename)s is done rendering."), payload)});
timelapseViewModel.requestData();
} else if (type == "MovieFailed") {
html = "<p>Rendering of timelapse " + payload.movie_basename + " failed with return code " + payload.returncode + "</p>";
html = "<p>" + _.sprintf(gettext("Rendering of timelapse %(movie_basename)s failedwith return code %(returncode)s"), payload) + "</p>";
html += pnotifyAdditionalInfo('<pre style="overflow: auto">' + payload.error + '</pre>');
new PNotify({title: "Rendering failed", text: html, type: "error", hide: false});
new PNotify({title: gettext("Rendering failed"), text: html, type: "error", hide: false});
} else if (type == "SlicingStarted") {
gcodeUploadProgress.addClass("progress-striped").addClass("active");
gcodeUploadProgressBar.css("width", "100%");
gcodeUploadProgressBar.text("Slicing ...");
gcodeUploadProgressBar.text(gettext("Slicing ..."));
} else if (type == "SlicingDone") {
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
gcodeUploadProgressBar.css("width", "0%");
gcodeUploadProgressBar.text("");
new PNotify({title: "Slicing done", text: "Sliced " + payload.stl + " to " + payload.gcode + ", took " + _.sprintf("%.2f", payload.time) + " seconds"});
new PNotify({title: gettext("Slicing done"), text: _.sprintf(gettext("Sliced %(stl)s to %(gcode)s, took %(time).2f seconds"), payload)});
gcodeFilesViewModel.requestData(payload.gcode);
} else if (type == "SlicingFailed") {
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
gcodeUploadProgressBar.css("width", "0%");
gcodeUploadProgressBar.text("");
html = "Could not slice " + payload.stl + " to " + payload.gcode + ": " + payload.reason;
new PNotify({title: "Slicing failed", text: html, type: "error", hide: false});
html = _.sprintf(gettext("Could not slice %(stl)s to %(gcode)s: %(reason)s"), payload);
new PNotify({title: gettext("Slicing failed"), text: html, type: "error", hide: false});
} else if (type == "TransferStarted") {
gcodeUploadProgress.addClass("progress-striped").addClass("active");
gcodeUploadProgressBar.css("width", "100%");
gcodeUploadProgressBar.text("Streaming ...");
gcodeUploadProgressBar.text(gettext("Streaming ..."));
} else if (type == "TransferDone") {
gcodeUploadProgress.removeClass("progress-striped").removeClass("active");
gcodeUploadProgressBar.css("width", "0%");
gcodeUploadProgressBar.text("");
new PNotify({title: "Streaming done", text: "Streamed " + payload.local + " to " + payload.remote + " on SD, took " + _.sprintf("%.2f", payload.time) + " seconds"});
new PNotify({title: gettext("Streaming done"), text: _.sprintf(gettext("Streamed %(local)s to %(remote)s on SD, took %(time).2f seconds"), payload)});
gcodeFilesViewModel.requestData(payload.remote, "sdcard");
}
break;

View file

@ -11,7 +11,7 @@ function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSor
self.searchFunction = undefined;
self.allItems = [];
0
self.items = ko.observableArray([]);
self.pageSize = ko.observable(filesPerPage);
self.currentPage = ko.observable(0);
@ -321,12 +321,12 @@ function formatDuration(seconds) {
var m = (seconds % 3600) / 60;
var h = seconds / 3600;
return _.sprintf("%02d:%02d:%02d", h, m, s);
return _.sprintf(gettext(/* L10N: duration format */ "%(hour)02d:%(minute)02d:%(second)02d"), {hour: h, minute: m, second: s});
}
function formatDate(unixTimestamp) {
if (!unixTimestamp) return "-";
return moment.unix(unixTimestamp).format("YYYY-MM-DD HH:mm");
return moment.unix(unixTimestamp).format(gettext(/* L10N: Date format */ "YYYY-MM-DD HH:mm"));
}
function formatTimeAgo(unixTimestamp) {
@ -336,20 +336,20 @@ function formatTimeAgo(unixTimestamp) {
function formatFilament(filament) {
if (!filament || !filament["length"]) return "-";
var result = _.sprintf("%.02fm", (filament["length"] / 1000));
var result = "%(length).02fm";
if (filament.hasOwnProperty("volume") && filament.volume) {
result += " / " + _.sprintf("%.02fcm³", filament["volume"]);
result += " / " + "%(volume).02fcm³";
}
return result;
return _.sprintf(result, {length: filament["length"] / 1000, volume: filament["volume"]});
}
function cleanTemperature(temp) {
if (!temp || temp < 10) return "off";
if (!temp || temp < 10) return gettext("off");
return temp;
}
function formatTemperature(temp) {
if (!temp || temp < 10) return "off";
if (!temp || temp < 10) return gettext("off");
return _.sprintf("%.1f&deg;C", temp);
}

View file

@ -1,4 +1,30 @@
$(function() {
//~~ Initialize i18n
var catalog = window["BABEL_TO_LOAD_" + LOCALE];
if (catalog === undefined) {
catalog = {messages: undefined, plural_expr: undefined, locale: undefined, domain: undefined}
}
babel.Translations.load(catalog).install();
moment.locale(LOCALE);
// Dummy translation requests for dynamic strings supplied by the backend
var dummyTranslations = [
// printer states
gettext("Offline"),
gettext("Opening serial port"),
gettext("Detecting serial port"),
gettext("Detecting baudrate"),
gettext("Connecting"),
gettext("Operational"),
gettext("Printing from SD"),
gettext("Sending file to SD"),
gettext("Printing"),
gettext("Paused"),
gettext("Closed"),
gettext("Transfering file to SD")
];
//~~ Initialize view models
var loginStateViewModel = new LoginStateViewModel();
var usersViewModel = new UsersViewModel(loginStateViewModel);
@ -92,7 +118,7 @@ $(function() {
}
function gcode_upload_fail(e, data) {
var error = "<p>Could not upload the file. Make sure that it is a GCODE file and has the extension \".gcode\" or \".gco\" or that it is an STL file with the extension \".stl\" and slicing support is enabled and configured.</p>";
var error = "<p>" + gettext("Could not upload the file. Make sure that it is a GCODE file and has the extension \".gcode\" or \".gco\" or that it is an STL file with the extension \".stl\" and slicing support is enabled and configured.") + "</p>";
error += pnotifyAdditionalInfo("<pre>" + data.jqXHR.responseText + "</pre>");
new PNotify({
title: "Upload failed",
@ -108,10 +134,10 @@ $(function() {
function gcode_upload_progress(e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
$("#gcode_upload_progress .bar").css("width", progress + "%");
$("#gcode_upload_progress .bar").text("Uploading ...");
$("#gcode_upload_progress .bar").text(gettext("Uploading ..."));
if (progress >= 100) {
$("#gcode_upload_progress").addClass("progress-striped").addClass("active");
$("#gcode_upload_progress .bar").text("Saving ...");
$("#gcode_upload_progress .bar").text(gettext("Saving ..."));
}
}

View file

@ -9,12 +9,12 @@ function AppearanceViewModel(settingsViewModel) {
return "OctoPrint: " + self.name();
else
return "OctoPrint";
})
});
self.title = ko.computed(function() {
if (self.name())
return self.name() + " [OctoPrint]";
else
return "OctoPrint";
})
});
}

View file

@ -21,9 +21,9 @@ function ConnectionViewModel(loginStateViewModel, settingsViewModel) {
self.buttonText = ko.computed(function() {
if (self.isErrorOrClosed())
return "Connect";
return gettext("Connect");
else
return "Disconnect";
return gettext("Disconnect");
})
self.previousIsOperational = undefined;

View file

@ -34,13 +34,13 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
// multiple extruders
for (var extruder = 0; extruder < numExtruders; extruder++) {
tools[extruder] = self._createToolEntry();
tools[extruder]["name"]("Tool " + extruder);
tools[extruder]["name"](gettext("Tool") + " " + extruder);
tools[extruder]["key"]("tool" + extruder);
}
} else {
// only one extruder, no need to add numbers
tools[0] = self._createToolEntry();
tools[0]["name"]("Hotend");
tools[0]["name"](gettext("Hotend"));
tools[0]["key"]("tool0");
}
@ -49,11 +49,11 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
self.fromCurrentData = function(data) {
self._processStateData(data.state);
}
};
self.fromHistoryData = function(data) {
self._processStateData(data.state);
}
};
self._processStateData = function(data) {
self.isErrorOrClosed(data.flags.closedOrError);
@ -63,13 +63,13 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
self.isError(data.flags.error);
self.isReady(data.flags.ready);
self.isLoading(data.flags.loading);
}
};
self.fromFeedbackCommandData = function(data) {
if (data.name in self.feedbackControlLookup) {
self.feedbackControlLookup[data.name](data.output);
}
}
};
self.requestData = function() {
$.ajax({
@ -80,18 +80,18 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
self._fromResponse(response);
}
});
}
};
self._fromResponse = function(response) {
self.controls(self._processControls(response.controls));
}
};
self._processControls = function(controls) {
for (var i = 0; i < controls.length; i++) {
controls[i] = self._processControl(controls[i]);
}
return controls;
}
};
self._processControl = function(control) {
if (control.type == "parametric_command" || control.type == "parametric_commands") {
@ -105,7 +105,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
control.children = self._processControls(control.children);
}
return control;
}
};
self.sendJogCommand = function(axis, multiplier, distance) {
if (typeof distance === "undefined")
@ -116,7 +116,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
var data = {
"command": "jog"
}
};
data[axis] = distance * multiplier;
$.ajax({
@ -126,13 +126,13 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
contentType: "application/json; charset=UTF-8",
data: JSON.stringify(data)
});
}
};
self.sendHomeCommand = function(axis) {
var data = {
"command": "home",
"axes": axis
}
};
$.ajax({
url: API_BASEURL + "printer/printhead",
@ -141,7 +141,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
contentType: "application/json; charset=UTF-8",
data: JSON.stringify(data)
});
}
};
self.sendExtrudeCommand = function() {
self._sendECommand(1);
@ -217,7 +217,7 @@ function ControlViewModel(loginStateViewModel, settingsViewModel) {
contentType: "application/json; charset=UTF-8",
data: JSON.stringify(data)
})
}
};
self.displayMode = function(customControl) {
switch (customControl.type) {

View file

@ -273,21 +273,21 @@ function GcodeFilesViewModel(printerStateViewModel, loginStateViewModel) {
if (data["gcodeAnalysis"]["filament"] && typeof(data["gcodeAnalysis"]["filament"]) == "object") {
var filament = data["gcodeAnalysis"]["filament"];
if (_.keys(filament).length == 1) {
output += "Filament: " + formatFilament(data["gcodeAnalysis"]["filament"]["tool" + 0]) + "<br>";
output += gettext("Filament") + ": " + formatFilament(data["gcodeAnalysis"]["filament"]["tool" + 0]) + "<br>";
} else if (_.keys(filament).length > 1) {
for (var toolKey in filament) {
if (!_.startsWith(toolKey, "tool") || !filament[toolKey] || !filament[toolKey].hasOwnProperty("length") || filament[toolKey]["length"] <= 0) continue;
output += "Filament (Tool " + toolKey.substr("tool".length) + "): " + formatFilament(filament[toolKey]) + "<br>";
output += gettext("Filament") + " (" + gettext("Tool") + " " + toolKey.substr("tool".length) + "): " + formatFilament(filament[toolKey]) + "<br>";
}
}
}
output += "Estimated Print Time: " + formatDuration(data["gcodeAnalysis"]["estimatedPrintTime"]) + "<br>";
output += gettext("Estimated Print Time") + ": " + formatDuration(data["gcodeAnalysis"]["estimatedPrintTime"]) + "<br>";
}
if (data["prints"] && data["prints"]["last"]) {
output += "Last Printed: " + formatTimeAgo(data["prints"]["last"]["date"]) + "<br>";
output += gettext("Last Printed") + ": " + formatTimeAgo(data["prints"]["last"]["date"]) + "<br>";
if (data["prints"]["last"]["lastPrintTime"]) {
output += "Last Print Time: " + formatDuration(data["prints"]["last"]["lastPrintTime"]);
output += gettext("Last Print Time") + ": " + formatDuration(data["prints"]["last"]["lastPrintTime"]);
}
}
return output;

View file

@ -34,9 +34,7 @@ function FirstRunViewModel() {
};
self.disableAccessControl = function() {
$("#confirmation_dialog .confirmation_dialog_message").html("If you disable Access Control <strong>and</strong> your OctoPrint " +
"installation is accessible from the internet, your printer <strong>will be accessible by everyone - " +
"that also includes the bad guys!</strong>");
$("#confirmation_dialog .confirmation_dialog_message").html(gettext("If you disable Access Control <strong>and</strong> your OctoPrint installation is accessible from the internet, your printer <strong>will be accessible by everyone - that also includes the bad guys!</strong>"));
$("#confirmation_dialog .confirmation_dialog_acknowledge").unbind("click");
$("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {
e.preventDefault();

View file

@ -10,15 +10,15 @@ function GcodeViewModel(loginStateViewModel, settingsViewModel) {
var text = "";
switch (self.ui_progress_type()) {
case "loading": {
text = "Loading... (" + self.ui_progress_percentage().toFixed(0) + "%)";
text = gettext("Loading...") + " (" + self.ui_progress_percentage().toFixed(0) + "%)";
break;
}
case "analyzing": {
text = "Analyzing... (" + self.ui_progress_percentage().toFixed(0) + "%)";
text = gettext("Analyzing...") + " (" + self.ui_progress_percentage().toFixed(0) + "%)";
break;
}
case "done": {
text = "Analyzed";
text = gettext("Analyzed");
break;
}
}
@ -322,17 +322,17 @@ function GcodeViewModel(loginStateViewModel, settingsViewModel) {
self.currentLayer = 0;
} else {
var output = [];
output.push("Model size is: " + model.width.toFixed(2) + "mm &times; " + model.depth.toFixed(2) + "mm &times; " + model.height.toFixed(2) + "mm");
output.push(gettext("Model size") + ": " + model.width.toFixed(2) + "mm &times; " + model.depth.toFixed(2) + "mm &times; " + model.height.toFixed(2) + "mm");
if (model.filament.length == 0) {
output.push("Total filament used: " + model.filament.toFixed(2) + "mm");
output.push(gettext("Total filament used") + ": " + model.filament.toFixed(2) + gettext("mm"));
} else {
for (var i = 0; i < model.filament.length; i++) {
output.push("Total filament used (Tool " + i + "): " + model.filament[i].toFixed(2) + "mm");
output.push(gettext("Total filament used") + " (" + gettext("Tool") + " " + i + "): " + model.filament[i].toFixed(2) + gettext("mm"));
}
}
output.push("Estimated print time: " + formatDuration(model.printTime));
output.push("Estimated layer height: " + model.layerHeight.toFixed(2) + "mm");
output.push("Layer count: " + model.layersPrinted.toFixed(0) + " printed, " + model.layersTotal.toFixed(0) + " visited");
output.push(gettext("Estimated print time") + ": " + formatDuration(model.printTime));
output.push(gettext("Estimated layer height") + ": " + model.layerHeight.toFixed(2) + gettext("mm"));
output.push(gettext("Layer count") + ": " + model.layersPrinted.toFixed(0) + " " + gettext(printed) + ", " + model.layersTotal.toFixed(0) + " " + gettext("visited"));
self.ui_modelInfo(output.join("<br>"));
@ -351,17 +351,17 @@ function GcodeViewModel(loginStateViewModel, settingsViewModel) {
self.currentCommand = [0, 1];
} else {
var output = [];
output.push("Layer number: " + (layer.number + 1));
output.push("Layer height (mm): " + layer.height);
output.push("GCODE commands in layer: " + layer.commands);
output.push(gettext("Layer number") + ": " + (layer.number + 1));
output.push(gettext("Layer height") + " (mm): " + layer.height);
output.push(gettext("GCODE commands in layer") + ": " + layer.commands);
if (layer.filament.length == 1) {
output.push("Filament used by layer: " + layer.filament[0].toFixed(2) + "mm");
output.push(gettext("Filament used by layer") + ": " + layer.filament[0].toFixed(2) + "mm");
} else {
for (var i = 0; i < layer.filament.length; i++) {
output.push("Filament used by layer (Tool " + i + "): " + layer.filament[i].toFixed(2) + "mm");
output.push(gettext("Filament used by layer") + " (" + gettext("Tool") + " " + i + "): " + layer.filament[i].toFixed(2) + "mm");
}
}
output.push("Print time for layer: " + formatDuration(layer.printTime));
output.push(gettext("Print time for layer") + ": " + formatDuration(layer.printTime));
self.ui_layerInfo(output.join("<br>"));

View file

@ -12,15 +12,15 @@ function LoginStateViewModel() {
if (self.loggedIn()) {
return "\"" + self.username() + "\"";
} else {
return "Login";
return gettext("Login");
}
})
});
self.subscribers = [];
self.subscribe = function(callback) {
if (callback === undefined) return;
self.subscribers.push(callback);
}
};
self.requestData = function() {
$.ajax({
@ -51,7 +51,7 @@ function LoginStateViewModel() {
_.each(self.subscribers, function(callback) { callback("logout", {}); });
}
}
};
self.login = function() {
var username = $("#login_user").val();
@ -67,21 +67,21 @@ function LoginStateViewModel() {
type: "POST",
data: {"user": username, "pass": password, "remember": remember},
success: function(response) {
new PNotify({title: "Login successful", text: "You are now logged in as \"" + response.name + "\"", type: "success"});
new PNotify({title: gettext("Login successful"), text: _.sprintf(gettext('You are now logged in as "%(username)s"'), {username: response.name}), type: "success"});
self.fromResponse(response);
},
error: function(jqXHR, textStatus, errorThrown) {
new PNotify({title: "Login failed", text: "User unknown or wrong password", type: "error"});
new PNotify({title: gettext("Login failed"), text: gettext("User unknown or wrong password"), type: "error"});
}
})
}
};
self.logout = function() {
$.ajax({
url: API_BASEURL + "logout",
type: "POST",
success: function(response) {
new PNotify({title: "Logout successful", text: "You are now logged out", type: "success"});
new PNotify({title: gettext("Logout successful"), text: gettext("You are now logged out"), type: "success"});
self.fromResponse(response);
}
})

View file

@ -14,12 +14,12 @@ function NavigationViewModel(loginStateViewModel, appearanceViewModel, settingsV
dataType: "json",
data: "action=" + action.action,
success: function() {
new PNotify({title: "Success", text: "The command \""+ action.name +"\" executed successfully", type: "success"});
new PNotify({title: "Success", text: _.sprintf(gettext("The command \"%(command)s\" executed successfully"), {command: action.name}), type: "success"});
},
error: function(jqXHR, textStatus, errorThrown) {
var error = "<p>The command \"" + action.name + "\" could not be executed.</p>";
var error = "<p>" + _.sprintf(gettext("The command \"%(command)s\" could not be executed."), {command: action.name}) + "</p>";
error += pnotifyAdditionalInfo("<pre>" + jqXHR.responseText + "</pre>");
new PNotify({title: "Error", text: error, type: "error", hide: false});
new PNotify({title: gettext("Error"), text: error, type: "error", hide: false});
}
})
};

View file

@ -63,9 +63,9 @@ function PrinterStateViewModel(loginStateViewModel) {
});
self.pauseString = ko.computed(function() {
if (self.isPaused())
return "Continue";
return gettext("Continue");
else
return "Pause";
return gettext("Pause");
});
self.timelapseString = ko.computed(function() {
@ -76,9 +76,9 @@ function PrinterStateViewModel(loginStateViewModel) {
var type = timelapse["type"];
if (type == "zchange") {
return "On Z Change";
return gettext("On Z Change");
} else if (type == "timed") {
return "Timed (" + timelapse["options"]["interval"] + "s)";
return gettext("Timed") + " (" + timelapse["options"]["interval"] + " " + gettext("sec") + ")";
} else {
return "-";
}
@ -104,7 +104,7 @@ function PrinterStateViewModel(loginStateViewModel) {
};
self._processStateData = function(data) {
self.stateString(data.text);
self.stateString(gettext(data.text));
self.isErrorOrClosed(data.flags.closedOrError);
self.isOperational(data.flags.operational);
self.isPaused(data.flags.paused);
@ -134,7 +134,7 @@ function PrinterStateViewModel(loginStateViewModel) {
if (!_.startsWith(key, "tool") || !data.filament[key] || !data.filament[key].hasOwnProperty("length") || data.filament[key].length <= 0) continue;
result.push({
name: ko.observable("Tool " + key.substr("tool".length)),
name: ko.observable(gettext("Tool") + " " + key.substr("tool".length)),
data: ko.observable(data.filament[key])
});
}
@ -163,7 +163,7 @@ function PrinterStateViewModel(loginStateViewModel) {
};
if (self.isPaused()) {
$("#confirmation_dialog .confirmation_dialog_message").text("This will restart the print job from the beginning.");
$("#confirmation_dialog .confirmation_dialog_message").text(gettext("This will restart the print job from the beginning."));
$("#confirmation_dialog .confirmation_dialog_acknowledge").unbind("click");
$("#confirmation_dialog .confirmation_dialog_acknowledge").click(function(e) {e.preventDefault(); $("#confirmation_dialog").modal("hide"); restartCommand(); });
$("#confirmation_dialog").modal("show");

View file

@ -11,7 +11,38 @@ function SettingsViewModel(loginStateViewModel, usersViewModel) {
self.appearance_name = ko.observable(undefined);
self.appearance_color = ko.observable(undefined);
self.appearance_available_colors = ko.observable(["default", "red", "orange", "yellow", "green", "blue", "violet", "black"]);
self.appearance_available_colors = ko.observable([
{key: "default", name: gettext("default")},
{key: "red", name: gettext("red")},
{key: "orange", name: gettext("orange")},
{key: "yellow", name: gettext("yellow")},
{key: "green", name: gettext("green")},
{key: "blue", name: gettext("blue")},
{key: "violet", name: gettext("violet")},
{key: "black", name: gettext("black")},
]);
self.appearance_colorName = function(color) {
switch (color) {
case "red":
return gettext("red");
case "orange":
return gettext("orange");
case "yellow":
return gettext("yellow");
case "green":
return gettext("green");
case "blue":
return gettext("blue");
case "violet":
return gettext("violet");
case "black":
return gettext("black");
case "default":
return gettext("default");
default:
return color;
}
};
self.printer_movementSpeedX = ko.observable(undefined);
self.printer_movementSpeedY = ko.observable(undefined);

View file

@ -19,7 +19,7 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
self.tools = ko.observableArray([]);
self.hasBed = ko.observable(true);
self.bedTemp = self._createToolEntry();
self.bedTemp["name"]("Bed");
self.bedTemp["name"](gettext("Bed"));
self.bedTemp["key"]("bed");
self.isErrorOrClosed = ko.observable(undefined);
@ -51,7 +51,7 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
if (tools.length <= extruder || !tools[extruder]) {
tools[extruder] = self._createToolEntry();
}
tools[extruder]["name"]("Tool " + extruder);
tools[extruder]["name"](gettext("Tool") + " " + extruder);
tools[extruder]["key"]("tool" + extruder);
}
} else {
@ -62,12 +62,12 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
if (tools.length < 1 || !tools[0]) {
tools[0] = self._createToolEntry();
}
tools[0]["name"]("Hotend");
tools[0]["name"](gettext("Hotend"));
tools[0]["key"]("tool0");
}
// print bed
heaterOptions["bed"] = {name: "Bed", color: "blue"};
heaterOptions["bed"] = {name: gettext("Bed"), color: "blue"};
// write back
self.heaterOptions(heaterOptions);
@ -98,9 +98,9 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
// convert to minutes
var diffInMins = Math.round(diff / (60 * 1000));
if (diffInMins == 0)
return "just now";
return gettext("just now");
else
return "- " + diffInMins + " min";
return "- " + diffInMins + " " + gettext("min");
}
},
legend: {
@ -238,12 +238,12 @@ function TemperatureViewModel(loginStateViewModel, settingsViewModel) {
var targetTemp = targets && targets.length ? formatTemperature(targets[targets.length - 1][1]) : "-";
data.push({
label: "Actual " + heaterOptions[type].name + ": " + actualTemp,
label: gettext("Actual") + " " + heaterOptions[type].name + ": " + actualTemp,
color: heaterOptions[type].color,
data: actuals
});
data.push({
label: "Target " + heaterOptions[type].name + ": " + targetTemp,
label: gettext("Target") + " " + heaterOptions[type].name + ": " + targetTemp,
color: pusher.color(heaterOptions[type].color).tint(0.5).html(),
data: targets
});

View file

@ -0,0 +1,7 @@
Note to developers
------------------
Do not edit the files in this directory directly, they are auto generated from the i18n PO files. If you want to
change any translations in here, please follow the translation guide on the Wiki:
https://github.com/foosel/OctoPrint/wiki/Translating-OctoPrint

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,160 @@
/**
* Babel JavaScript Support
*
* Copyright (C) 2008-2011 Edgewall Software
* All rights reserved.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at http://babel.edgewall.org/wiki/License.
*
* This software consists of voluntary contributions made by many
* individuals. For the exact contribution history, see the revision
* history and logs, available at http://babel.edgewall.org/log/.
*/
/**
* A simple module that provides a gettext like translation interface.
* The catalog passed to load() must be a object conforming to this
* interface::
*
* {
* messages: an object of {msgid: translations} items where
* translations is an array of messages or a single
* string if the message is not pluralizable.
* plural_expr: the plural expression for the language.
* locale: the identifier for this locale.
* domain: the name of the domain.
* }
*
* Missing elements in the object are ignored.
*
* Typical usage::
*
* var translations = babel.Translations.load(...).install();
*/
var babel = new function() {
var defaultPluralExpr = function(n) { return n == 1 ? 0 : 1; };
var formatRegex = /%?%(?:\(([^\)]+)\))?([disr])/g;
/**
* A translations object implementing the gettext interface
*/
var Translations = this.Translations = function(locale, domain) {
this.messages = {};
this.locale = locale || 'unknown';
this.domain = domain || 'messages';
this.pluralexpr = defaultPluralExpr;
};
/**
* Create a new translations object from the catalog and return it.
* See the babel-module comment for more details.
*/
Translations.load = function(catalog) {
var rv = new Translations();
rv.load(catalog);
return rv;
};
Translations.prototype = {
/**
* translate a single string.
*/
gettext: function(string) {
var translated = this.messages[string];
if (typeof translated == 'undefined')
return string;
return (typeof translated == 'string') ? translated : translated[0];
},
/**
* translate a pluralizable string
*/
ngettext: function(singular, plural, n) {
var translated = this.messages[singular];
if (typeof translated == 'undefined')
return (n == 1) ? singular : plural;
return translated[this.pluralexpr(n)];
},
/**
* Install this translation document wide. After this call, there are
* three new methods on the window object: _, gettext and ngettext
*/
install: function() {
var self = this;
window.gettext = function(string) {
return self.gettext(string);
};
window.ngettext = function(singular, plural, n) {
return self.ngettext(singular, plural, n);
};
return this;
},
/**
* Works like Translations.load but updates the instance rather
* then creating a new one.
*/
load: function(catalog) {
if (catalog.messages)
this.update(catalog.messages);
if (catalog.plural_expr)
this.setPluralExpr(catalog.plural_expr);
if (catalog.locale)
this.locale = catalog.locale;
if (catalog.domain)
this.domain = catalog.domain;
return this;
},
/**
* Updates the translations with the object of messages.
*/
update: function(mapping) {
for (var key in mapping)
if (mapping.hasOwnProperty(key))
this.messages[key] = mapping[key];
return this;
},
/**
* Sets the plural expression
*/
setPluralExpr: function(expr) {
this.pluralexpr = new Function('n', 'return +(' + expr + ')');
return this;
}
};
/**
* A python inspired string formatting function. Supports named and
* positional placeholders and "s", "d" and "i" as type characters
* without any formatting specifications.
*
* Examples::
*
* babel.format(_('Hello %s'), name)
* babel.format(_('Progress: %(percent)s%%'), {percent: 100})
*/
this.format = function() {
var arg, string = arguments[0], idx = 0;
if (arguments.length == 1)
return string;
else if (arguments.length == 2 && typeof arguments[1] == 'object')
arg = arguments[1];
else {
arg = [];
for (var i = 1, n = arguments.length; i != n; ++i)
arg[i - 1] = arguments[i];
}
return string.replace(formatRegex, function(all, name, type) {
if (all[0] == all[1]) return all.substring(1);
var value = arg[name || idx++];
return (type == 'i' || type == 'd') ? +value : value;
});
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -3,14 +3,10 @@
<div id="offline_overlay_wrapper">
<div class="container">
<div class="hero-unit">
<h1>Server is offline</h1>
<p id="offline_overlay_message">
The server appears to be offline, at least I'm not getting any response from it. I'll try to reconnect
automatically <strong>over the next couple of minutes</strong>, however you are welcome to try a manual reconnect
anytime using the button below.
</p>
<h1>{{ _('Server is offline') }}</h1>
<p id="offline_overlay_message"></p>
<p>
<a class="btn btn-primary btn-large" id="offline_overlay_reconnect">Attempt to reconnect</a>
<a class="btn btn-primary btn-large" id="offline_overlay_reconnect">{{ _('Attempt to reconnect') }}</a>
</p>
</div>
</div>
@ -21,12 +17,12 @@
<div id="drop_overlay_background"></div>
<div id="drop_overlay_wrapper">
{% if enableSdSupport %}
<div class="dropzone" id="drop_locally"><span class="centered"><i class="icon-upload-alt"></i><br>Upload locally</span></div>
<div class="dropzone" id="drop_locally"><span class="centered"><i class="icon-upload-alt"></i><br>{{ _('Upload locally') }}</span></div>
<div class="dropzone_background" id="drop_locally_background"></div>
<div class="dropzone" id="drop_sd"><span class="centered"><i class="icon-upload-alt"></i><br>Upload to SD<br><small data-bind="visible: !isSdReady()">(SD not initialized)</small></span></div>
<div class="dropzone" id="drop_sd"><span class="centered"><i class="icon-upload-alt"></i><br>{{ _('Upload to SD') }}<br><small data-bind="visible: !isSdReady()">({{ _('SD not initialized') }})</small></span></div>
<div class="dropzone_background" id="drop_sd_background"></div>
{% else %}
<div class="dropzone" id="drop"><span class="centered"><i class="icon-upload-alt"></i><br>Upload</span></div>
<div class="dropzone" id="drop"><span class="centered"><i class="icon-upload-alt"></i><br>{{ _('Upload') }}</span></div>
<div class="dropzone_background" id="drop_background"></div>
{% endif %}
</div>
@ -35,29 +31,29 @@
<div id="confirmation_dialog" class="modal hide fade">
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
<h3>Are you sure?</h3>
<h3>{{ _('Are you sure?') }}</h3>
</div>
<div class="modal-body">
<p class="confirmation_dialog_message"></p>
<p>Are you sure you want to proceed?</p>
<p>{{ _('Are you sure you want to proceed?') }}</p>
</div>
<div class="modal-footer">
<a href="#" class="btn" data-dismiss="modal" aria-hidden="true">Cancel</a>
<a href="#" class="btn btn-danger confirmation_dialog_acknowledge">Proceed</a>
<a href="#" class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Cancel') }}</a>
<a href="#" class="btn btn-danger confirmation_dialog_acknowledge">{{ _('Proceed') }}</a>
</div>
</div>
<div id="first_run_dialog" class="modal hide fade" data-backdrop="static" data-keyboard="false">
<div class="modal-header">
<h3><i class="icon-warning-sign"></i> Configure Access Control</h3>
<h3><i class="icon-warning-sign"></i> {{ _('Configure Access Control') }}</h3>
</div>
<div class="modal-body">
<p>
{% trans %}<p>
<strong>Please read the following, it is very important for your printer's health!</strong>
</p>
<p>
OctoPrint by default now ships with Access Control enabled, meaning you won't be able to do anything with the
printer unless you login first as a configured user. This is to <strong>prevent strangers - possibly with
printer unless you login first as a configured user. This is just to <strong>prevent strangers - possibly with
malicious intent - to gain access to your printer</strong> via the internet or another untrustworthy network
and using it in such a way that it is damaged or worse (i.e. causes a fire).
</p>
@ -65,39 +61,39 @@
It looks like you haven't configured access control yet. Please <strong>set up an username and password</strong> for the
initial administrator account who will have full access to both the printer and OctoPrint's settings, then click
on "Keep Access Control Enabled":
</p>
</p>{% endtrans %}
<form class="form-horizontal">
<div class="control-group" data-bind="css: {success: validUsername()}">
<label class="control-label" for="first_run_username">Username</label>
<label class="control-label" for="first_run_username">{{ _('Username') }}</label>
<div class="controls">
<input type="text" class="input-medium" data-bind="value: username, valueUpdate: 'afterkeydown'">
</div>
</div>
<div class="control-group" data-bind="css: {success: validPassword()}">
<label class="control-label" for="first_run_username">Password</label>
<label class="control-label" for="first_run_username">{{ _('Password') }}</label>
<div class="controls">
<input type="password" class="input-medium" data-bind="value: password, valueUpdate: 'afterkeydown'">
</div>
</div>
<div class="control-group" data-bind="css: {error: passwordMismatch(), success: validPassword() && !passwordMismatch()}">
<label class="control-label" for="first_run_username">Confirm Password</label>
<label class="control-label" for="first_run_username">{{ _('Confirm Password') }}</label>
<div class="controls">
<input type="password" class="input-medium" data-bind="value: confirmedPassword, valueUpdate: 'afterkeydown'">
<span class="help-inline" data-bind="visible: passwordMismatch()">Passwords don't match</span>
<span class="help-inline" data-bind="visible: passwordMismatch()">{{ _('Passwords do not match') }}</span>
</div>
</div>
</form>
<p>
{% trans %}<p>
<strong>Note:</strong> In case that your OctoPrint installation is only accessible from within a trustworthy network and you don't
need Access Control for other reasons, you may alternatively disable Access Control. You should only
do this if you are absolutely certain that only people you know and trust will be able to connect to it.
</p>
<p>
<strong>Do NOT underestimate the risk of an unsecured access from the internet to your printer!</strong>
</p>
</p>{% endtrans %}
</div>
<div class="modal-footer">
<a href="#" class="btn btn-danger" data-bind="click: disableAccessControl">Disable Access Control</a>
<a href="#" class="btn btn-primary" data-bind="click: keepAccessControl, enable: validData(), css: {disabled: !validData()}">Keep Access Control Enabled</a>
<a href="#" class="btn btn-danger" data-bind="click: disableAccessControl">{{ _('Disable Access Control') }}</a>
<a href="#" class="btn btn-primary" data-bind="click: keepAccessControl, enable: validData(), css: {disabled: !validData()}">{{ _('Keep Access Control Enabled') }}</a>
</div>
</div>

View file

@ -42,6 +42,7 @@
var UI_API_KEY = "{{ uiApiKey }}";
var VERSION = "{{ version }}";
var LOCALE = "{{ g.locale }}";
</script>
</head>
<body>
@ -53,13 +54,13 @@
<ul class="nav pull-right">
<li style="display: none;" data-bind="visible: loginState.isAdmin">
<a id="navbar_show_settings" class="pull-right" href="#settings_dialog">
<i class="icon-wrench"></i> Settings
<i class="icon-wrench"></i> {{ _('Settings') }}
</a>
</li>
{% if enableSystemMenu %}
<li class="dropdown" style="display: none" data-bind="visible: loginState.isAdmin">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-off"></i> System
<i class="icon-off"></i> {{ _('System') }}
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-bind="foreach: systemActions">
@ -70,22 +71,22 @@
{% if enableAccessControl %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<i class="icon-user"></i> <span data-bind="text: loginState.userMenuText">Login</span>
<i class="icon-user"></i> <span data-bind="text: loginState.userMenuText">{{ _('Login') }}</span>
<b class="caret"></b>
</a>
<div id="login_dropdown_loggedout" style="padding: 15px" class="dropdown-menu" data-bind="css: {hide: loginState.loggedIn(), 'dropdown-menu': !loginState.loggedIn()}">
<label for="login_user">Username</label>
<input type="text" id="login_user" placeholder="Username">
<label for="login_pass">Password</label>
<input type="password" id="login_pass" placeholder="Password">
<label for="login_user">{{ _('Username') }}</label>
<input type="text" id="login_user" placeholder="{{ _('Username') }}">
<label for="login_pass">{{ _('Password') }}</label>
<input type="password" id="login_pass" placeholder="{{ _('Password') }}">
<label class="checkbox">
<input type="checkbox" id="login_remember"> Remember me
<input type="checkbox" id="login_remember"> {{ _('Remember me') }}
</label>
<button class="btn btn-block btn-primary" id="login_button" data-bind="click: loginState.login">Login</button>
<button class="btn btn-block btn-primary" id="login_button" data-bind="click: loginState.login">{{ _('Login') }}</button>
</div>
<ul id="login_dropdown_loggedin" class="hide" data-bind="css: {hide: !loginState.loggedIn(), 'dropdown-menu': loginState.loggedIn()}">
<li><a href="#" id="change_password_button" data-bind="click: function() { users.showChangePasswordDialog(loginState.currentUser()); }">Change Password</a></li>
<li><a href="#" id="logout_button" data-bind="click: loginState.logout">Logout</a></li>
<li><a href="#" id="change_password_button" data-bind="click: function() { users.showChangePasswordDialog(loginState.currentUser()); }">{{ _('Change Password') }}</a></li>
<li><a href="#" id="logout_button" data-bind="click: loginState.logout">{{ _('Logout') }}</a></li>
</ul>
</li>
{% endif %}
@ -103,68 +104,68 @@
</div>
<div class="accordion-body collapse in" id="connection">
<div class="accordion-inner">
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">Serial Port</label>
<label for="connection_ports" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">{{ _('Serial Port') }}</label>
<select id="connection_ports" data-bind="options: portOptions, optionsCaption: 'AUTO', value: selectedPort, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">Baudrate</label>
<label for="connection_baudrates" data-bind="css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()">{{ _('Baudrate') }}</label>
<select id="connection_baudrates" data-bind="options: baudrateOptions, optionsCaption: 'AUTO', value: selectedBaudrate, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"></select>
<label class="checkbox">
<input type="checkbox" id="connection_save" data-bind="checked: saveSettings, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> Save connection settings
<input type="checkbox" id="connection_save" data-bind="checked: saveSettings, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> {{ _('Save connection settings') }}
</label>
<label class="checkbox">
<input type="checkbox" id="connection_autoconnect" data-bind="checked: settings.serial_autoconnect, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> Auto-connect on server startup
<input type="checkbox" id="connection_autoconnect" data-bind="checked: settings.serial_autoconnect, css: {disabled: !isErrorOrClosed()}, enable: isErrorOrClosed() && loginState.isUser()"> {{ _('Auto-connect on server startup') }}
</label>
<button class="btn btn-block" id="printer_connect" data-bind="click: connect, text: buttonText(), enable: loginState.isUser()">Connect</button>
<button class="btn btn-block" id="printer_connect" data-bind="click: connect, text: buttonText(), enable: loginState.isUser()">{{ _('Connect') }}</button>
</div>
</div>
</div>
<div class="accordion-group" id="state_accordion">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#state"><i class="icon-info-sign"></i> State</a>
<a class="accordion-toggle" data-toggle="collapse" href="#state"><i class="icon-info-sign"></i> {{ _('State') }}</a>
</div>
<div class="accordion-body collapse in" id="state">
<div class="accordion-inner">
Machine State: <strong data-bind="text: stateString"></strong><br>
File: <strong data-bind="text: filename"></strong>&nbsp;<strong data-bind="visible: sd">(SD)</strong><br>
Timelapse: <strong data-bind="text: timelapseString"></strong><br>
{{ _('Machine State') }}: <strong data-bind="text: stateString"></strong><br>
{{ _('File') }}: <strong data-bind="text: filename"></strong>&nbsp;<strong data-bind="visible: sd">(SD)</strong><br>
{{ _('Timelapse') }}: <strong data-bind="text: timelapseString"></strong><br>
<!-- ko foreach: filament -->
<span data-bind="text: 'Filament (' + name() + '): '"></span><strong data-bind="text: formatFilament(data())"></strong><br>
<!-- /ko -->
Approx. Total Print Time: <strong data-bind="text: estimatedPrintTimeString"></strong><br>
Print Time: <strong data-bind="text: printTimeString"></strong><br>
Print Time Left: <strong data-bind="text: printTimeLeftString"></strong><br>
Printed: <strong data-bind="text: byteString"></strong><br>
{{ _('Approx. Total Print Time') }}: <strong data-bind="text: estimatedPrintTimeString"></strong><br>
{{ _('Print Time') }}: <strong data-bind="text: printTimeString"></strong><br>
{{ _('Print Time Left') }}: <strong data-bind="text: printTimeLeftString"></strong><br>
{{ _('Printed') }}: <strong data-bind="text: byteString"></strong><br>
<div class="progress">
<div class="bar" id="job_progressBar" data-bind="style: { width: progressString() + '%' }"></div>
</div>
<div class="row-fluid print-control" style="display: none;" data-bind="visible: loginState.isUser">
<button class="btn btn-primary span4" data-bind="click: print, enable: isOperational() && isReady() && !isPrinting() && loginState.isUser(), css: {'btn-danger': isPaused()}" id="job_print"><i class="icon-white" data-bind="css: {'icon-print': !isPaused(), 'icon-undo': isPaused()}"></i> <span data-bind="text: (isPaused() ? 'Restart' : 'Print')">Print</span></button>
<button class="btn span4" id="job_pause" data-bind="click: pause, enable: isOperational() && (isPrinting() || isPaused()) && loginState.isUser(), css: {active: isPaused()}"><i class="icon-pause"></i> <span>Pause</span></button>
<button class="btn span4" id="job_cancel" data-bind="click: cancel, enable: isOperational() && (isPrinting() || isPaused()) && loginState.isUser()"><i class="icon-stop"></i> Cancel</button>
<button class="btn btn-primary span4" data-bind="click: print, enable: isOperational() && isReady() && !isPrinting() && loginState.isUser(), css: {'btn-danger': isPaused()}" id="job_print"><i class="icon-white" data-bind="css: {'icon-print': !isPaused(), 'icon-undo': isPaused()}"></i> <span data-bind="text: (isPaused() ? '{{ _('Restart') }}' : '{{ _('Print') }}')">{{ _('Print') }}</span></button>
<button class="btn span4" id="job_pause" data-bind="click: pause, enable: isOperational() && (isPrinting() || isPaused()) && loginState.isUser(), css: {active: isPaused()}"><i class="icon-pause"></i> <span>{{ _('Pause') }}</span></button>
<button class="btn span4" id="job_cancel" data-bind="click: cancel, enable: isOperational() && (isPrinting() || isPaused()) && loginState.isUser()"><i class="icon-stop"></i> {{ _('Cancel') }}</button>
</div>
</div>
</div>
</div>
<div class="accordion-group" id="files_accordion">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" href="#files"><i class="icon-list"></i> Files</a>
<a class="accordion-toggle" data-toggle="collapse" href="#files"><i class="icon-list"></i> {{ _('Files') }}</a>
<div class="settings-trigger accordion-heading-button btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<span class="icon-wrench"></span>
</a>
<ul class="dropdown-menu">
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> Sort by name (ascending)</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('upload'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'upload' ? 'visible' : 'hidden'}"></i> Sort by upload date (descending)</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('size'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> Sort by file size (descending)</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('name'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'name' ? 'visible' : 'hidden'}"></i> {{ _('Sort by name') }} ({{ _('ascending') }})</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('upload'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'upload' ? 'visible' : 'hidden'}"></i> {{ _('Sort by upload date') }} ({{ _('descending') }})</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.changeSorting('size'); }"><i class="icon-ok" data-bind="style: {visibility: listHelper.currentSorting() == 'size' ? 'visible' : 'hidden'}"></i> {{ _('Sort by file size') }} ({{ _('descending') }})</a></li>
{% if enableSdSupport %}
<li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('local'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'local') ? 'visible' : 'hidden'}"></i> Only show files stored locally</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('sd'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'sd') ? 'visible' : 'hidden'}"></i> Only show files stored on SD</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('local'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'local') ? 'visible' : 'hidden'}"></i> {{ _('Only show files stored locally') }}</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('sd'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'sd') ? 'visible' : 'hidden'}"></i> {{ _('Only show files stored on SD') }}</a></li>
{% endif %}
<li class="divider"></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('printed'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'printed') ? 'visible' : 'hidden'}"></i> Hide successfully printed files</a></li>
<li><a href="#" data-bind="click: function() { $root.listHelper.toggleFilter('printed'); }"><i class="icon-ok" data-bind="style: {visibility: _.contains(listHelper.currentFilters(), 'printed') ? 'visible' : 'hidden'}"></i> {{ _('Hide successfully printed files') }}</a></li>
</ul>
</div>
@ -174,9 +175,9 @@
<span class="icon-sd-black-14"></span>
</a>
<ul class="dropdown-menu">
<li data-bind="visible: !isSdReady()"><a href="#" data-bind="click: function() { $root.initSdCard(); }"><i class="icon-flag"></i> Initialize SD card</a></li>
<li data-bind="visible: isSdReady()"><a href="#" data-bind="click: function() { $root.refreshSdFiles(); }"><i class="icon-refresh"></i> Refresh SD files</a></li>
<li data-bind="visible: isSdReady()"><a href="#" data-bind="click: function() { $root.releaseSdCard(); }"><i class="icon-eject"></i> Release SD card</a></li>
<li data-bind="visible: !isSdReady()"><a href="#" data-bind="click: function() { $root.initSdCard(); }"><i class="icon-flag"></i> {{ _('Initialize SD card') }}</a></li>
<li data-bind="visible: isSdReady()"><a href="#" data-bind="click: function() { $root.refreshSdFiles(); }"><i class="icon-refresh"></i> {{ _('Refresh SD files') }}</a></li>
<li data-bind="visible: isSdReady()"><a href="#" data-bind="click: function() { $root.releaseSdCard(); }"><i class="icon-eject"></i> {{ _('Release SD card') }}</a></li>
</ul>
</div>
{% endif %}
@ -184,43 +185,43 @@
<div class="accordion-body collapse in overflow_visible" id="files">
<div class="accordion-inner">
<form class="form-search">
<input type="text" class="input-block search-query" data-bind="value: searchQuery, valueUpdate: 'input'" placeholder="Search...">
<input type="text" class="input-block search-query" data-bind="value: searchQuery, valueUpdate: 'input'" placeholder="{{ _('Search...') }}">
</form>
<div class="gcode_files" data-bind="slimScrolledForeach: listHelper.paginatedItems">
<div class="entry" data-bind="attr: { id: $root.getEntryId($data) }">
<div class="title" data-bind="css: $root.getSuccessClass($data), style: { 'font-weight': $root.listHelper.isSelected($data) ? 'bold' : 'normal' }, text: name"></div>
<div class="uploaded">Uploaded: <span data-bind="text: formatTimeAgo(date)"></span></div>
<div class="size">Size: <span data-bind="text: formatSize(size)"></span></div>
<div class="uploaded">{{ _('Uploaded') }}: <span data-bind="text: formatTimeAgo(date)"></span></div>
<div class="size">{{ _('Size') }}: <span data-bind="text: formatSize(size)"></span></div>
<div class="additionalInfo hide" data-bind="html: $root.getAdditionalData($data)"></div>
<div class="btn-group action-buttons">
<div class="btn btn-mini toggleAdditionalData" data-bind="click: function() { if ($root.enableAdditionalData($data)) { $root.toggleAdditionalData($data); } else { return; } }, css: { disabled: !$root.enableAdditionalData($data) }"><i class="icon-chevron-down"></i></div>
<a class="btn btn-mini" data-bind="attr: {href: $root.downloadLink($data), css: {disabled: !$root.downloadLink($data)}}"><i class="icon-download-alt" title="Download"></i></a>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableRemove($data)) { $root.removeFile($data.name); } else { return; } }, css: {disabled: !$root.enableRemove($data)}"><i class="icon-trash" title="Remove"></i></div>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data.name, false); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"><i class="icon-folder-open" title="Load"></i></div>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data.name, true); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"><i class="icon-print" title="Load and Print"></i></div>
<a class="btn btn-mini" data-bind="attr: {href: $root.downloadLink($data), css: {disabled: !$root.downloadLink($data)}}"><i class="icon-download-alt" title="{{ _('Download') }}"></i></a>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableRemove($data)) { $root.removeFile($data.name); } else { return; } }, css: {disabled: !$root.enableRemove($data)}"><i class="icon-trash" title="{{ _('Remove') }}"></i></div>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data.name, false); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"><i class="icon-folder-open" title="{{ _('Load') }}"></i></div>
<div class="btn btn-mini" data-bind="click: function() { if ($root.enableSelect($data)) { $root.loadFile($data.name, true); } else { return; } }, css: {disabled: !$root.enableSelect($data)}"><i class="icon-print" title="{{ _('Load and Print') }}"></i></div>
</div>
</div>
</div>
<div class="muted text-right">
<small>Free: <span data-bind="text: freeSpaceString"></span></small>
<small>{{ _('Free') }}: <span data-bind="text: freeSpaceString"></span></small>
</div>
<div style="display: none;" data-bind="visible: loginState.isUser">
<div class="row-fluid upload-buttons">
{% if enableSdSupport %}
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>Upload</span>
<span>{{ _('Upload') }}</span>
<input id="gcode_upload" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser()">
</span>
<span class="btn btn-primary fileinput-button span6" data-bind="css: {disabled: !$root.loginState.isUser() || !$root.isSdReady()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>Upload to SD</span>
<span>{{ _('Upload to SD') }}</span>
<input id="gcode_upload_sd" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser() && isSdReady()">
</span>
{% else %}
<span class="btn btn-primary fileinput-button span12" data-bind="css: {disabled: !$root.loginState.isUser()}" style="margin-bottom: 10px">
<i class="icon-upload-alt icon-white"></i>
<span>Upload</span>
<span>{{ _('Upload') }}</span>
<input id="gcode_upload" type="file" name="file" class="fileinput-button" data-bind="enable: loginState.isUser()">
</span>
{% endif %}
@ -229,7 +230,7 @@
<div class="bar" style="width: 0%"></div>
</div>
<div>
<small>Hint: You can also drag and drop files on this page to upload them.</small>
<small>{{ _('Hint: You can also drag and drop files on this page to upload them.') }}</small>
</div>
</div>
</div>
@ -239,11 +240,11 @@
<div class="span8 tabbable">
<ul class="nav nav-tabs" id="tabs">
<li class="active"><a href="#temp" data-toggle="tab">Temperature</a></li>
<li><a href="#control" data-toggle="tab">Control</a></li>
{% if enableGCodeVisualizer %}<li><a href="#gcode" data-toggle="tab">GCode Viewer</a></li>{% endif %}
<li><a href="#term" data-toggle="tab">Terminal</a></li>
{% if enableTimelapse %}<li><a href="#timelapse" data-toggle="tab">Timelapse</a></li>{% endif %}
<li class="active"><a href="#temp" data-toggle="tab">{{ _('Temperature') }}</a></li>
<li><a href="#control" data-toggle="tab">{{ _('Control') }}</a></li>
{% if enableGCodeVisualizer %}<li><a href="#gcode" data-toggle="tab">{{ _('GCode Viewer') }}</a></li>{% endif %}
<li><a href="#term" data-toggle="tab">{{ _('Terminal') }}</a></li>
{% if enableTimelapse %}<li><a href="#timelapse" data-toggle="tab">{{ _('Timelapse') }}</a></li>{% endif %}
</ul>
<div class="tab-content">
@ -258,9 +259,9 @@
<table class="table table-bordered table-hover" style="table-layout: fixed; width: 100%; margin-top: 20px">
<tr>
<th style="width: 18%"></th>
<th style="width: 12%; text-align: right">Actual</th>
<th style="width: 35%">Target</th>
<th style="width: 35%">Offset</th>
<th style="width: 12%; text-align: right">{{ _('Actual') }}</th>
<th style="width: 35%">{{ _('Target') }}</th>
<th style="width: 35%">{{ _('Offset') }}</th>
</tr>
<!-- ko foreach: tools -->
<tr data-bind="template: { name: 'temprow-template' }"></tr>
@ -276,7 +277,7 @@
<input type="text" class="input-mini text-right tempInput" data-bind="attr: {placeholder: cleanTemperature(target()) }, value: newTarget, enable: $root.isOperational() && $root.loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'target', $data);} }">
<span class="add-on">&deg;C</span>
<div class="btn-group">
<button type="submit" data-bind="click: $parent.setTarget, enable: $root.isOperational() && $root.loginState.isUser()" class="btn">Set</button>
<button type="submit" data-bind="click: $parent.setTarget, enable: $root.isOperational() && $root.loginState.isUser()" class="btn">{{ _('Set') }}</button>
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: $root.isOperational() && $root.loginState.isUser()">
<span class="caret"></span>
</button>
@ -288,7 +289,7 @@
<!-- /ko -->
<li class="divider"></li>
<li>
<a href="#" data-bind="click: $root.setTargetToZero">Off</a>
<a href="#" data-bind="click: $root.setTargetToZero">{{ _('Off') }}</a>
</li>
</ul>
</div>
@ -298,7 +299,7 @@
<div class="input-append">
<input type="number" min="-50" max="50" class="input-mini text-right tempInput" data-bind="attr: {placeholder: offset}, value: newOffset, enable: $root.isOperational() && $root.loginState.isUser(), event: { keyup: function(d, e) {$root.handleEnter(e, 'offset', $data);} }">
<span class="add-on">&deg;C</span>
<button type="submit" data-bind="click: $root.setOffset, enable: $root.isOperational() && $root.loginState.isUser()" class="btn">Set</button>
<button type="submit" data-bind="click: $root.setOffset, enable: $root.isOperational() && $root.loginState.isUser()" class="btn">{{ _('Set') }}</button>
</div>
</td>
</script>
@ -356,28 +357,28 @@
<div>
<div class="btn-group control-box">
<button class="btn dropdown-toggle" data-toggle="dropdown" data-bind="enable: isOperational() && !isPrinting() && !isPaused() && loginState.isUser()">
Select Tool...
{{ _('Select Tool...') }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" data-bind="foreach: tools">
<li><a href="#" data-bind="click: $root.sendSelectToolCommand, text: 'Select ' + name(), enable: $root.isOperational() && !$root.isPrinting() && !$root.isPaused() && $root.loginState.isUser()"></a></li>
<li><a href="#" data-bind="click: $root.sendSelectToolCommand, text: name(), enable: $root.isOperational() && !$root.isPrinting() && !$root.isPaused() && $root.loginState.isUser()"></a></li>
</ul>
</div>
<div class="input-append control-box">
<input type="text" class="input-mini text-right" data-bind="value: extrusionAmount, enable: isOperational() && !isPrinting() && loginState.isUser(), attr: {placeholder: settings.printer_defaultExtrusionLength}">
<span class="add-on">mm</span>
</div>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendExtrudeCommand() }">Extrude</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendRetractCommand() }">Retract</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendExtrudeCommand() }">{{ _('Extrude') }}</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendRetractCommand() }">{{ _('Retract') }}</button>
</div>
</div>
<!-- General control panel -->
<div class="jog-panel" style="display: none;" data-bind="visible: loginState.isUser">
<h1>General</h1>
<h1>{{ _('General') }}</h1>
<div>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M18'}) }">Motors off</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106'}) }">Fans on</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106 S0'}) }">Fans off</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && !isPrinting() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M18'}) }">{{ _('Motors off') }}</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106'}) }">{{ _('Fans on') }}</button>
<button class="btn btn-block control-box" data-bind="enable: isOperational() && loginState.isUser(), click: function() { $root.sendCustomCommand({type:'command',command:'M106 S0'}) }">{{ _('Fans off') }}</button>
</div>
</div>
@ -441,38 +442,38 @@
<p>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_syncProgress">Sync with job progress
<input type="checkbox" data-bind="checked: renderer_syncProgress">{{ _('Sync with job progress') }}
</label>
</p>
<p>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_centerViewport">Center viewport on model
<input type="checkbox" data-bind="checked: renderer_centerViewport">{{ _('Center viewport on model') }}
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_zoomOnModel">Zoom in on model
<input type="checkbox" data-bind="checked: renderer_zoomOnModel">{{ _('Zoom in on model') }}
</label>
</p>
<p>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showMoves">Show moves
<input type="checkbox" data-bind="checked: renderer_showMoves">{{ _('Show moves') }}
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showRetracts">Show retracts
<input type="checkbox" data-bind="checked: renderer_showRetracts">{{ _('Show retracts') }}
</label>
</p>
<p>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showPrevious">Also show previous layer
<input type="checkbox" data-bind="checked: renderer_showPrevious">{{ _('Also show previous layer') }}
</label>
<label class="checkbox">
<input type="checkbox" data-bind="checked: renderer_showNext">Also show next layer
<input type="checkbox" data-bind="checked: renderer_showNext">{{ _('Also show next layer') }}
</label>
</p>
<p>
<button class="btn btn-block" data-bind="click: reload, enable: enableReload">Reload</button>
<button class="btn btn-block" data-bind="click: reload, enable: enableReload">{{ _('Reload') }}</button>
</p>
</div>
</div>
@ -499,7 +500,7 @@
<div class="tab-pane" id="term">
<pre id="terminal-output" class="pre-scrollable"></pre>
<label class="checkbox">
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> Autoscroll
<input type="checkbox" id="terminal-autoscroll" data-bind="checked: autoscrollEnabled"> {{ _('Autoscroll') }}
</label>
<div data-bind="foreach: filters">
<label class="checkbox">
@ -509,57 +510,57 @@
<div class="input-append" style="display: none;" data-bind="visible: loginState.isUser">
<input type="text" id="terminal-command" data-bind="value: command, event: { keyup: function(d,e) { return handleKeyUp(e); }, keydown: function(d,e) { return handleKeyDown(e); } }, enable: isOperational() && loginState.isUser()">
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">Send</button>
<button class="btn" type="button" id="terminal-send" data-bind="click: sendCommand, enable: isOperational() && loginState.isUser()">{{ _('Send') }}</button>
</div>
</div>
{% if enableTimelapse %}
<div class="tab-pane" id="timelapse">
<div style="display: none;" data-bind="visible: loginState.isUser">
<h1>Timelapse Configuration</h1>
<h1>{{ _('Timelapse Configuration') }}</h1>
<label for="webcam_timelapse_mode">Timelapse Mode</label>
<label for="webcam_timelapse_mode">{{ _('Timelapse Mode') }}</label>
<select id="webcam_timelapse_mode" data-bind="value: timelapseType, enable: isOperational() && !isPrinting() && loginState.isUser()">
<option value="off">Off</option>
<option value="zchange">On Z Change</option>
<option value="timed">Timed</option>
<option value="off">{{ _('Off') }}</option>
<option value="zchange">{{ _('On Z Change') }}</option>
<option value="timed">{{ _('Timed') }}</option>
</select>
<label for="webcam_timelapse_postRoll">Timelapse post roll (in rendered seconds)</label>
<label for="webcam_timelapse_postRoll">{{ _('Timelapse post roll (in rendered seconds)') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_postRoll" data-bind="value: timelapsePostRoll, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<span class="add-on">sec</span>
<span class="add-on">{{ _('sec') }}</span>
</div>
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled">
<label for="webcam_timelapse_interval">Interval</label>
<label for="webcam_timelapse_interval">{{ _('Interval') }}</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser()">
<span class="add-on">sec</span>
<span class="add-on">{{ _('sec') }}</span>
</div>
</div>
<div data-bind="visible: loginState.isAdmin">
<label class="checkbox">
<input type="checkbox" data-bind="checked: persist"> Save as default
<input type="checkbox" data-bind="checked: persist"> {{ _('Save as default') }}
</label>
</div>
<div>
<button class="btn" data-bind="click: save, enable: saveButtonEnabled">Save config</button>
<button class="btn" data-bind="click: save, enable: saveButtonEnabled">{{ _('Save config') }}</button>
</div>
</div>
<h1>Finished Timelapses</h1>
<h1>{{ _('Finished Timelapses') }}</h1>
<div class="pull-right">
<small>Sort by: <a href="#" data-bind="click: function() { listHelper.changeSorting('name'); }">Name (ascending)</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('creation'); }">Creation date (descending)</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('size'); }">Size (descending)</a></small>
<small>{{ _('Sort by') }}: <a href="#" data-bind="click: function() { listHelper.changeSorting('name'); }">{{ _('Name') }} ({{ _('ascending') }})</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('creation'); }">{{ _('Creation date') }} ({{ _('descending') }})</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('size'); }">{{ _('Size') }} ({{ _('descending') }})</a></small>
</div>
<table class="table table-striped table-hover table-condensed table-hover" id="timelapse_files">
<thead>
<tr>
<th class="timelapse_files_name">Name</th>
<th class="timelapse_files_size">Size</th>
<th class="timelapse_files_action">Action</th>
<th class="timelapse_files_name">{{ _('Name') }}</th>
<th class="timelapse_files_size">{{ _('Size') }}</th>
<th class="timelapse_files_action">{{ _('Action') }}</th>
</tr>
</thead>
<tbody data-bind="foreach: listHelper.paginatedItems">
@ -588,13 +589,13 @@
</div>
<div class="footer">
<ul class="pull-left muted">
<li><small>Version: <span class="version">{{ version }}</span></small></li>
<li><small>{{ _('Version') }}: <span class="version">{{ version }}</span></small></li>
</ul>
<ul class="pull-right">
<li><a href="http://octoprint.org"><i class="icon-home"></i> Homepage</a></li>
<li><a href="https://github.com/foosel/OctoPrint/"><i class="icon-download"></i> Sourcecode</a></li>
<li><a href="https://github.com/foosel/OctoPrint/wiki"><i class="icon-book"></i> Documentation</a></li>
<li><a href="https://github.com/foosel/OctoPrint/issues"><i class="icon-flag"></i> Bugs and Requests</a></li>
<li><a href="http://octoprint.org"><i class="icon-home"></i> {{ _('Homepage') }}</a></li>
<li><a href="https://github.com/foosel/OctoPrint/"><i class="icon-download"></i> {{ _('Sourcecode') }}</a></li>
<li><a href="https://github.com/foosel/OctoPrint/wiki"><i class="icon-book"></i> {{ _('Documentation') }}</a></li>
<li><a href="https://github.com/foosel/OctoPrint/issues"><i class="icon-flag"></i> {{ _('Bugs and Requests') }}</a></li>
</ul>
</div>
</div>
@ -607,6 +608,7 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore-min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/underscore.string.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/knockout.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/babel.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/avltree.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-modalmanager.js') }}"></script>
@ -620,7 +622,7 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.fileupload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.slimscroll.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/sockjs-0.3.4.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/moment.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/moment-with-locales.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/pusher.color.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/detectmobilebrowser.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/md5.min.js') }}"></script>
@ -650,6 +652,12 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/app/main.js') }}"></script>
<!-- /Include OctoPrint files -->
<!-- Include i18n language files -->
{% if g.locale != 'en' %}
<script type="text/javascript" src="{{ url_for('static', filename='js/i18n/%s.js' % g.locale) }}"></script>
{% endif %}
<!-- /Include i18n language files -->
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/ui.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/gCodeReader.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/renderer.js') }}"></script>

View file

@ -1,39 +1,39 @@
<div id="settings_dialog" class="modal hide fade container" tabindex="-1" role="dialog" aria-labelledby="settings_dialog_label" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3 id="settings_dialog_label">OctoPrint Settings</h3>
<h3 id="settings_dialog_label">{{ _('OctoPrint Settings') }}</h3>
</div>
<div class="modal-body">
<div class="tabbable">
<ul class="nav nav-list span4" id="settingsTabs">
<li class="nav-header">Printer</li>
<li class="active"><a href="#settings_serialConnection" data-toggle="tab">Serial Connection</a></li>
<li><a href="#settings_printerParameters" data-toggle="tab">Printer Parameters</a></li>
<li><a href="#settings_temperature" data-toggle="tab">Temperatures</a></li>
<li><a href="#settings_terminalFilters" data-toggle="tab">Terminal filters</a></li>
<li class="nav-header">Features</li>
<li><a href="#settings_features" data-toggle="tab">Features</a></li>
<li><a href="#settings_webcam" data-toggle="tab">Webcam</a></li>
<li><a href="#settings_cura" data-toggle="tab">Cura</a></li>
{% if enableAccessControl %}<li><a href="#settings_users" data-toggle="tab">Access Control</a></li>{% endif %}
<li><a href="#settings_api" data-toggle="tab">Api</a></li>
<li class="nav-header">{{ _('Printer') }}</li>
<li class="active"><a href="#settings_serialConnection" data-toggle="tab">{{ _('Serial Connection') }}</a></li>
<li><a href="#settings_printerParameters" data-toggle="tab">{{ _('Printer Parameters') }}</a></li>
<li><a href="#settings_temperature" data-toggle="tab">{{ _('Temperatures') }}</a></li>
<li><a href="#settings_terminalFilters" data-toggle="tab">{{ _('Terminal filters') }}</a></li>
<li class="nav-header">{{ _('Features') }}</li>
<li><a href="#settings_features" data-toggle="tab">{{ _('Features') }}</a></li>
<li><a href="#settings_webcam" data-toggle="tab">{{ _('Webcam') }}</a></li>
<li><a href="#settings_cura" data-toggle="tab">{{ _('Cura') }}</a></li>
{% if enableAccessControl %}<li><a href="#settings_users" data-toggle="tab">{{ _('Access Control') }}</a></li>{% endif %}
<li><a href="#settings_api" data-toggle="tab">{{ _('API') }}</a></li>
<li class="nav-header">OctoPrint</li>
<li><a href="#settings_folder" data-toggle="tab">Folders</a></li>
<li><a href="#settings_appearance" data-toggle="tab">Appearance</a></li>
<li><a href="#settings_logs" data-toggle="tab">Logs</a></li>
<li><a href="#settings_folder" data-toggle="tab">{{ _('Folders') }}</a></li>
<li><a href="#settings_appearance" data-toggle="tab">{{ _('Appearance') }}</a></li>
<li><a href="#settings_logs" data-toggle="tab">{{ _('Logs') }}</a></li>
</ul>
<div class="tab-content span8">
<div class="tab-pane active" id="settings_serialConnection">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-serialPort">Serial Port</label>
<label class="control-label" for="settings-serialPort">{{ _('Serial Port') }}</label>
<div class="controls">
<select id="settings-serialPort" data-bind="options: serial_portOptions, optionsCaption: 'AUTO', value: serial_port"></select>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-baudrate">Baudrate</label>
<label class="control-label" for="settings-baudrate">{{ _('Baudrate') }}</label>
<div class="controls">
<select id="settings-baudrate" data-bind="options: serial_baudrateOptions, optionsCaption: 'AUTO', value: serial_baudrate"></select>
</div>
@ -41,12 +41,12 @@
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: serial_autoconnect" id="settings-serialAutoconnect"> Auto-connect to printer on server start
<input type="checkbox" data-bind="checked: serial_autoconnect" id="settings-serialAutoconnect"> {{ _('Auto-connect to printer on server start') }}
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-serialTimeoutCommunication">Communication timeout</label>
<label class="control-label" for="settings-serialTimeoutCommunication">{{ _('Communication timeout') }}</label>
<div class="controls">
<div class="input-append">
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutCommunication" id="settings-serialTimeoutCommunication">
@ -55,7 +55,7 @@
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-movementSpeedE">Temperature timeout</label>
<label class="control-label" for="settings-movementSpeedE">{{ _('Temperature timeout') }}</label>
<div class="controls">
<div class="input-append">
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutTemperature" id="settings-serialTimeoutTemperature">
@ -64,7 +64,7 @@
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-serialTimeoutSdStatus">SD status timeout</label>
<label class="control-label" for="settings-serialTimeoutSdStatus">{{ _('SD status timeout') }}</label>
<div class="controls">
<div class="input-append">
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutSdStatus" id="settings-serialTimeoutSdStatus">
@ -73,7 +73,7 @@
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-serialTimeoutConnection">Connection timeout</label>
<label class="control-label" for="settings-serialTimeoutConnection">{{ _('Connection timeout') }}</label>
<div class="controls">
<div class="input-append">
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutConnection" id="settings-serialTimeoutConnection">
@ -82,7 +82,7 @@
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-serialTimeoutDetection">Autodetection timeout</label>
<label class="control-label" for="settings-serialTimeoutDetection">{{ _('Autodetection timeout') }}</label>
<div class="controls">
<div class="input-append">
<input type="number" step="any" min="0" class="input-mini text-right" data-bind="value: serial_timeoutDetection" id="settings-serialTimeoutDetection">
@ -93,7 +93,7 @@
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: serial_log" id="settings-serialLog"> Log communication to serial.log (might negatively impact performance) <span class="label label-important">Warning</span>
<input type="checkbox" data-bind="checked: serial_log" id="settings-serialLog"> {{ _('Log communication to serial.log (might negatively impact performance)') }} <span class="label label-important">{{ _('Warning') }}</span>
</label>
</div>
</div>
@ -102,39 +102,39 @@
<div class="tab-pane" id="settings_printerParameters">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label">Axis</label>
<label class="control-label">{{ _('Axis') }}</label>
<div class="controls form-inline">
<label>X:</label>
<label>{{ _('X') }}:</label>
<div class="input-append">
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedX" id="settings-movementSpeedX">
<span class="add-on">mm/min</span>
</div>
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_invertX" id="settings-printerInvertX"> Invert control
<input type="checkbox" data-bind="checked: printer_invertX" id="settings-printerInvertX"> {{ _('Invert control') }}
</label>
</div>
<div class="controls form-inline">
<label>Y:</label>
<label>{{ _('Y') }}:</label>
<div class="input-append">
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedY" id="settings-movementSpeedY">
<span class="add-on">mm/min</span>
</div>
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_invertY" id="settings-printerInvertY"> Invert control
<input type="checkbox" data-bind="checked: printer_invertY" id="settings-printerInvertY"> {{ _('Invert control') }}
</label>
</div>
<div class="controls form-inline">
<label>Z:</label>
<label>{{ _('Z') }}:</label>
<div class="input-append">
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedZ" id="settings-movementSpeedZ">
<span class="add-on">mm/min</span>
</div>
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_invertZ" id="settings-printerInvertZ"> Invert control
<input type="checkbox" data-bind="checked: printer_invertZ" id="settings-printerInvertZ"> {{ _('Invert control') }}
</label>
</div>
<div class="controls form-inline">
<label>E:</label>
<label>{{ _('E') }}:</label>
<div class="input-append">
<input type="number" class="input-mini text-right" data-bind="value: printer_movementSpeedE" id="settings-movementSpeedE">
<span class="add-on">mm/min</span>
@ -142,19 +142,19 @@
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-defaultExtrusionLength">Default extrusion length</label>
<label class="control-label" for="settings-defaultExtrusionLength">{{ _('Default extrusion length') }}</label>
<div class="controls">
<input type="number" class="input-mini text-right" min="1" data-bind="value: printer_defaultExtrusionLength" id="settings-defaultExtrusionLength">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-numExtruders">Number of Extruders</label>
<label class="control-label" for="settings-numExtruders">{{ _('Number of Extruders') }}</label>
<div class="controls">
<input type="number" class="input-mini text-right" min="1" max="5" data-bind="value: printer_numExtruders" id="settings-numExtruders">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-extruderOffsets">Extruder Offsets</label>
<label class="control-label" for="settings-extruderOffsets">{{ _('Extruder Offsets') }}</label>
<!-- ko foreach: ko_printer_extruderOffsets -->
<div class="controls form-inline">
<label>X:</label>
@ -171,21 +171,21 @@
<!-- /ko -->
</div>
<div class="control-group">
<label class="control-label" for="settings-bedSize">Bed Size</label>
<label class="control-label" for="settings-bedSize">{{ _('Bed Size') }}</label>
<div class="controls form-inline" data-bind="ifnot: printer_bedCircular">
<label>X:</label>
<label>{{ _('X') }}:</label>
<div class="input-append">
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: printer_bedDimensionX" id="settings-bedX">
<span class="add-on">mm</span>
</div>
<label>Y:</label>
<label>{{ _('Y') }}:</label>
<div class="input-append">
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: printer_bedDimensionY" id="settings-bedY">
<span class="add-on">mm</span>
</div>
</div>
<div class="controls form-inline" data-bind="if: printer_bedCircular">
<label>Radius:</label>
<label>{{ _('Radius') }}:</label>
<div class="input-append">
<input type="number" step="0.01" class="input-mini text-right" data-bind="value: printer_bedDimensionR" id="settings-bedR">
<span class="add-on">mm</span>
@ -193,7 +193,7 @@
</div>
<div class="controls form-inline">
<label class="checkbox">
<input type="checkbox" data-bind="checked: printer_bedCircular" id="settings-bedCircular">Circular
<input type="checkbox" data-bind="checked: printer_bedCircular" id="settings-bedCircular"> {{ _('Circular') }}
</label>
</div>
</div>
@ -202,25 +202,25 @@
<div class="tab-pane" id="settings_webcam">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-webcamStreamUrl">Stream URL</label>
<label class="control-label" for="settings-webcamStreamUrl">{{ _('Stream URL') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: webcam_streamUrl" id="settings-webcamStreamUrl">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-webcamStreamUrl">Snapshot URL</label>
<label class="control-label" for="settings-webcamStreamUrl">{{ _('Snapshot URL') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: webcam_snapshotUrl" id="settings-webcamSnapshotUrl">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-webcamStreamUrl">Path to FFMPEG</label>
<label class="control-label" for="settings-webcamStreamUrl">{{ _('Path to FFMPEG') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: webcam_ffmpegPath" id="settings-webcamFfmpegPath">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-webcamBitrate">Timelapse bitrate</label>
<label class="control-label" for="settings-webcamBitrate">{{ _('Timelapse bitrate') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: webcam_bitrate" id="settings-webcamBitrate">
</div>
@ -228,19 +228,19 @@
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: webcam_watermark" id="settings-webcamWatermark"> Enable OctoPrint watermark in timelapse movies
<input type="checkbox" data-bind="checked: webcam_watermark" id="settings-webcamWatermark"> {{ _('Enable OctoPrint watermark in timelapse movies') }}
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: webcam_flipH" id="settings-webcamFlipH"> Flip webcam horizontally
<input type="checkbox" data-bind="checked: webcam_flipH" id="settings-webcamFlipH"> {{ _('Flip webcam horizontally') }}
</label>
</div>
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: webcam_flipV" id="settings-webcamFlipV"> Flip webcam vertically
<input type="checkbox" data-bind="checked: webcam_flipV" id="settings-webcamFlipV"> {{ _('Flip webcam vertically') }}
</label>
</div>
</div>
@ -251,56 +251,56 @@
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_temperatureGraph" id="settings-featureTemperatureGraph"> Enable Temperature Graph
<input type="checkbox" data-bind="checked: feature_temperatureGraph" id="settings-featureTemperatureGraph"> {{ _('Enable Temperature Graph') }}
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_gcodeViewer" id="settings-featureGcodeViewer"> Enable GCode Visualizer
<input type="checkbox" data-bind="checked: feature_gcodeViewer" id="settings-featureGcodeViewer"> {{ _('Enable GCode Visualizer') }}
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_sdSupport" id="settings-featureSdSupport"> Enable SD support
<input type="checkbox" data-bind="checked: feature_sdSupport" id="settings-featureSdSupport"> {{ _('Enable SD support') }}
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_sdAlwaysAvailable" id="settings-featureSdAlwaysAvailable"> Always assume SD card is present <span class="label">Repetier</span>
<input type="checkbox" data-bind="checked: feature_sdAlwaysAvailable" id="settings-featureSdAlwaysAvailable"> {{ _('Always assume SD card is present') }} <span class="label">{{ _('Repetier') }}</span>
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_waitForStart" id="settings-featureWaitForStart"> Wait for <code>start</code> on connect
<input type="checkbox" data-bind="checked: feature_waitForStart" id="settings-featureWaitForStart"> {{ _('Wait for <code>start</code> on connect') }}
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_alwaysSendChecksum" id="settings-featureAlwaysSendChecksum"> Send a checksum with <strong>every</strong> command <span class="label">Repetier</span>
<input type="checkbox" data-bind="checked: feature_alwaysSendChecksum" id="settings-featureAlwaysSendChecksum"> {{ _('Send a checksum with <strong>every</strong> command') }} <span class="label">{{ _('Repetier') }}</span>
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_repetierTargetTemp" id="settings-featureRepetierTargetTemp"> Support <code>TargetExtr%n</code>/<code>TargetBed</code> target temperature format <span class="label">Repetier</span>
<input type="checkbox" data-bind="checked: feature_repetierTargetTemp" id="settings-featureRepetierTargetTemp"> {{ _('Support <code>TargetExtr%%n</code>/<code>TargetBed</code> target temperature format') }} <span class="label">{{ _('Repetier') }}</span>
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: feature_swallowOkAfterResend" id="settings-swallowOkAfterResend"> Swallow the first "ok" after a resend response
<input type="checkbox" data-bind="checked: feature_swallowOkAfterResend" id="settings-swallowOkAfterResend"> {{ _('Swallow the first "ok" after a resend response') }}
</label>
</div>
</div>
@ -309,31 +309,31 @@
<div class="tab-pane" id="settings_folder">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-folderUploads">Upload Folder</label>
<label class="control-label" for="settings-folderUploads">{{ _('Upload Folder') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: folder_uploads" id="settings-folderUploads">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-folderTimelapse">Timelapse Folder</label>
<label class="control-label" for="settings-folderTimelapse">{{ _('Timelapse Folder') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: folder_timelapse" id="settings-folderTimelapse">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-folderTimelapseTemp">Timelapse Temp Folder</label>
<label class="control-label" for="settings-folderTimelapseTemp">{{ _('Timelapse Temp Folder') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: folder_timelapseTmp" id="settings-folderTimelapseTemp">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-folderLogs">Logs Folder</label>
<label class="control-label" for="settings-folderLogs">{{ _('Logs Folder') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: folder_logs" id="settings-folderLogs">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-watchedLogs">Watched Folder</label>
<label class="control-label" for="settings-watchedLogs">{{ _('Watched Folder') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: folder_watched" id="settings-folderWatched">
</div>
@ -343,8 +343,8 @@
<div class="tab-pane" id="settings_temperature">
<form class="form-horizontal">
<div class="row-fluid">
<div class="offset3 span3"><h4>Extruder</h4></div>
<div class="span3"><h4>Bed</h4></div>
<div class="offset3 span3"><h4>{{ _('Extruder') }}</h4></div>
<div class="span3"><h4>{{ _('Bed') }}</h4></div>
</div>
<div data-bind="foreach: temperature_profiles">
<div class="row-fluid" style="margin-bottom: 5px">
@ -374,8 +374,8 @@
<div class="tab-pane" id="settings_terminalFilters">
<form class="form-horizontal">
<div class="row-fluid">
<div class="span4"><h4>Name</h4></div>
<div class="span6"><h4>RegExp <small><a href="https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions">?</a></small></h4></div>
<div class="span4"><h4>{{ _('Name') }}</h4></div>
<div class="span6"><h4>{{ _('RegExp') }} <small><a href="https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions">?</a></small></h4></div>
</div>
<div data-bind="foreach: terminalFilters">
<div class="row-fluid" style="margin-bottom: 5px">
@ -400,15 +400,15 @@
<div class="tab-pane" id="settings_appearance">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-appearanceName">Title</label>
<label class="control-label" for="settings-appearanceName">{{ _('Title') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: appearance_name" id="settings-appearanceName">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-appearanceColor">Color</label>
<label class="control-label" for="settings-appearanceColor">{{ _('Color') }}</label>
<div class="controls">
<select id="settings-appearanceColor" data-bind="value: appearance_color, options: appearance_available_colors">
<select id="settings-appearanceColor" data-bind="value: appearance_color, options: appearance_available_colors, optionsText: 'name', optionsValue: 'key'">
</select>
</div>
</div>
@ -417,19 +417,19 @@
<div class="tab-pane" id="settings_cura">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-curaEnabled">Enable slicing via Cura</label>
<label class="control-label" for="settings-curaEnabled">{{ _('Enable slicing via Cura') }}</label>
<div class="controls">
<input type="checkbox" data-bind="checked: cura_enabled">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-curaPath">Path to Cura</label>
<label class="control-label" for="settings-curaPath">{{ _('Path to Cura') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: cura_path">
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-curaConfig">Path to Cura config</label>
<label class="control-label" for="settings-curaConfig">{{ _('Path to Cura config') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: cura_config">
</div>
@ -441,19 +441,19 @@
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="settings-apiEnabled" data-bind="checked: api_enabled"> Enable
<input type="checkbox" id="settings-apiEnabled" data-bind="checked: api_enabled"> {{ _('Enable') }}
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="settings-apiCors" data-bind="checked: api_allowCrossOrigin"> Allow <a href="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing">Cross Origin Resource Sharing (CORS)</a>
<input type="checkbox" id="settings-apiCors" data-bind="checked: api_allowCrossOrigin"> {{ _('Allow <a href="%(url)s">Cross Origin Resource Sharing (CORS)</a>', url = "https://en.wikipedia.org/wiki/Cross-origin_resource_sharing") }}
</label>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-apiKey">Apikey</label>
<label class="control-label" for="settings-apiKey">{{ _('API Key') }}</label>
<div class="controls">
<input type="text" class="input-block-level" data-bind="value: api_key" id="settings-apikey">
</div>
@ -466,19 +466,19 @@
<table class="table table-condensed table-hover" id="system_users">
<thead>
<tr>
<th class="settings_users_name">Name</th>
<th class="settings_users_active">Active</th>
<th class="settings_users_admin">Admin</th>
<th class="settings_users_actions">Action</th>
<th class="settings_users_name">{{ _('Name') }}</th>
<th class="settings_users_active">{{ _('Active') }}</th>
<th class="settings_users_admin">{{ _('Admin') }}</th>
<th class="settings_users_actions">{{ _('Action') }}</th>
</tr>
</thead>
<tbody data-bind="foreach: users.listHelper.paginatedItems">
<tr>
<td class="settings_users_name"><span data-bind="text: name"></span><span class="muted" data-bind="visible: $root.api_enabled() && apikey"><br /><small>Apikey: <span data-bind="text: apikey"></span></small></span></td>
<td class="settings_users_name"><span data-bind="text: name"></span><span class="muted" data-bind="visible: $root.api_enabled() && apikey"><br /><small>{{ _('API Key') }}: <span data-bind="text: apikey"></span></small></span></td>
<td class="settings_users_active"><i data-bind="css: { 'icon-check': active, 'icon-check-empty': !active }"></i></td>
<td class="settings_users_admin"><i data-bind="css: { 'icon-check': admin, 'icon-check-empty': !admin }"></i></td>
<td class="settings_users_actions" class="system_users_action">
<a href="#" class="icon-pencil" title="Update User" data-bind="click: function() { $root.users.showEditUserDialog($data); }"></a>&nbsp;|&nbsp;<a href="#" class="icon-key" title="Change password" data-bind="click: function() { $root.users.showChangePasswordDialog($data); }"></a>&nbsp;|&nbsp;<a href="#" class="icon-trash" title="Delete user" data-bind="click: function() { $root.users.removeUser($data); }"></a>
<a href="#" class="icon-pencil" title="{{ _('Update User') }}" data-bind="click: function() { $root.users.showEditUserDialog($data); }"></a>&nbsp;|&nbsp;<a href="#" class="icon-key" title="{{ _('Change password') }}" data-bind="click: function() { $root.users.showChangePasswordDialog($data); }"></a>&nbsp;|&nbsp;<a href="#" class="icon-trash" title="{{ _('Delete user') }}" data-bind="click: function() { $root.users.removeUser($data); }"></a>
</td>
</tr>
</tbody>
@ -495,114 +495,114 @@
</ul>
</div>
<button title="Add user" class="btn" data-bind="click: $root.users.showAddUserDialog"><i class="icon-plus"></i> Create new user</button>
<button title="Add user" class="btn" data-bind="click: $root.users.showAddUserDialog"><i class="icon-plus"></i> {{ _('Add user') }}</button>
<!-- Modals for user management -->
<div id="settings-usersDialogAddUser" class="modal hide fade">
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
<h3>Create new user</h3>
<h3>{{ _('Add user') }}</h3>
</div>
<div class="modal-body">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-usersDialogAddUserName">Username</label>
<label class="control-label" for="settings-usersDialogAddUserName">{{ _('Username') }}</label>
<div class="controls">
<input type="text" class="input-block-level" id="settings-usersDialogAddUserName" data-bind="value: $root.users.editorUsername" required>
</div>
</div>
<div class="control-group">
<label class="control-label" for="settings-usersDialogAddUserPassword1">Password</label>
<label class="control-label" for="settings-usersDialogAddUserPassword1">{{ _('Password') }}</label>
<div class="controls">
<input type="password" class="input-block-level" id="settings-usersDialogAddUserPassword1" data-bind="value: $root.users.editorPassword" required>
</div>
</div>
<div class="control-group" data-bind="css: {error: $root.users.editorPasswordMismatch()}">
<label class="control-label" for="settings-usersDialogAddUserPassword2">Repeat Password</label>
<label class="control-label" for="settings-usersDialogAddUserPassword2">{{ _('Repeat Password') }}</label>
<div class="controls">
<input type="password" class="input-block-level" id="settings-usersDialogAddUserPassword2" data-bind="value: $root.users.editorRepeatedPassword, valueUpdate: 'afterkeydown'" required>
<span class="help-inline" data-bind="visible: $root.users.editorPasswordMismatch()">Passwords do not match</span>
<span class="help-inline" data-bind="visible: $root.users.editorPasswordMismatch()">{{ _('Passwords do not match') }}</span>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="settings-usersDialogAddUserActive" data-bind="checked: $root.users.editorActive"> Active
<input type="checkbox" id="settings-usersDialogAddUserActive" data-bind="checked: $root.users.editorActive"> {{ _('Active') }}
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="settings-usersDialogAddUserAdmin" data-bind="checked: $root.users.editorAdmin"> Admin
<input type="checkbox" id="settings-usersDialogAddUserAdmin" data-bind="checked: $root.users.editorAdmin"> {{ _('Admin') }}
</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Abort</button>
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmAddUser(); }, enable: !$root.users.editorPasswordMismatch()">Confirm</button>
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Abort') }}</button>
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmAddUser(); }, enable: !$root.users.editorPasswordMismatch()">{{ _('Confirm') }}</button>
</div>
</div>
<div id="settings-usersDialogEditUser" class="modal hide fade">
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
<h3>Edit user "<span data-bind="text: $root.users.editorUsername"></span>"</h3>
<h3>{{ _('Edit user "%(user)s"', user = '<span data-bind="text: $root.users.editorUsername"></span>') }}</h3>
</div>
<div class="modal-body">
<form class="form-horizontal">
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="settings-usersDialogEditUserActive" data-bind="checked: $root.users.editorActive"> Active
<input type="checkbox" id="settings-usersDialogEditUserActive" data-bind="checked: $root.users.editorActive"> {{ _('Active') }}
</label>
</div>
</div>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="settings-usersDialogEditUserAdmin" data-bind="checked: $root.users.editorAdmin"> Admin
<input type="checkbox" id="settings-usersDialogEditUserAdmin" data-bind="checked: $root.users.editorAdmin"> {{ _('Admin') }}
</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Abort</button>
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmEditUser(); }">Confirm</button>
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Abort') }}</button>
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmEditUser(); }">{{ _('Confirm') }}</button>
</div>
</div>
<div id="settings-usersDialogChangePassword" class="modal hide fade">
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
<h3>Change password for user "<span data-bind="text: $root.users.editorUsername"></span>"</h3>
<h3>{{ _('Change password for user "%(user)s"', user='<span data-bind="text: $root.users.editorUsername"></span>') }}</h3>
</div>
<div class="modal-body">
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-usersDialogChangePasswordPassword1">New Password</label>
<label class="control-label" for="settings-usersDialogChangePasswordPassword1">{{ _('New Password') }}</label>
<div class="controls">
<input type="password" class="input-block-level" id="settings-usersDialogChangePasswordPassword1" data-bind="value: $root.users.editorPassword" required>
</div>
</div>
<div class="control-group" data-bind="css: {error: $root.users.editorPasswordMismatch()}">
<label class="control-label" for="settings-usersDialogChangePasswordPassword2">Repeat Password</label>
<label class="control-label" for="settings-usersDialogChangePasswordPassword2">{{ _('Repeat Password') }}</label>
<div class="controls">
<input type="password" class="input-block-level" id="settings-usersDialogChangePasswordPassword2" data-bind="value: $root.users.editorRepeatedPassword, valueUpdate: 'afterkeydown'" required>
<span class="help-inline" data-bind="visible: $root.users.editorPasswordMismatch()">Passwords do not match</span>
<span class="help-inline" data-bind="visible: $root.users.editorPasswordMismatch()">{{ _('Passwords do not match') }}</span>
</div>
</div>
<fieldset data-bind="visible: api_enabled">
<legend>Apikey</legend>
<div class="control-group">
<label class="control-label">Current Apikey</label>
<label class="control-label">{{ _('Current API Key') }}</label>
<div class="controls">
<div class="input-append">
<input type="text" class="input-block-level uneditable-input" data-bind="value: $root.users.editorApikey, attr: {placeholder: 'N/A'}">
<input type="text" class="input-block-level uneditable-input" data-bind="value: $root.users.editorApikey, attr: {placeholder: '{{ _('N/A') }}'}">
<a class="btn" title="Generate new Apikey" data-bind="click: function() { $root.users.confirmGenerateApikey(); }"><i class="icon-refresh"></i></a>
<a class="btn btn-danger" title="Delete Apikey" data-bind="click: function() { $root.users.confirmDeleteApikey(); }"><i class="icon-trash"></i></a>
</div>
@ -613,8 +613,8 @@
</form>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Abort</button>
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmChangePassword(); }, enable: !$root.users.editorPasswordMismatch()">Confirm</button>
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Abort') }}</button>
<button class="btn btn-primary" data-bind="click: function() { $root.users.confirmChangePassword(); }, enable: !$root.users.editorPasswordMismatch()">{{ _('Confirm') }}</button>
</div>
</div>
@ -623,20 +623,20 @@
<div class="tab-pane" id="settings_logs" data-bind="allowBindings: false">
<div id="logs">
<h1>Logs</h1>
<h1>{{ _('Logs') }}</h1>
<div class="pull-right">
<small>
Sort by: <a href="#" data-bind="click: function() { listHelper.changeSorting('name'); }">Name (ascending)</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('modification'); }">Modification date (descending)</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('size'); }">Size (descending)</a>
{{ _('Sort by') }}: <a href="#" data-bind="click: function() { listHelper.changeSorting('name'); }">{{ _('Name') }} ({{ _('ascending') }})</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('modification'); }">{{ _('Modification date') }} ({{ _('descending') }})</a> | <a href="#" data-bind="click: function() { listHelper.changeSorting('size'); }">{{ _('Size') }} ({{ _('descending') }})</a>
</small>
</div>
<table class="table table-striped table-hover table-condensed table-hover" id="log_files">
<thead>
<tr>
<th class="settings_logs_name">Name</th>
<th class="settings_logs_size">Size</th>
<th class="settings_logs_date">Date</th>
<th class="settings_logs_action">Action</th>
<th class="settings_logs_name">{{ _('Name') }}</th>
<th class="settings_logs_size">{{ _('Size') }}</th>
<th class="settings_logs_date">{{ _('Date') }}</th>
<th class="settings_logs_action">{{ _('Action') }}</th>
</tr>
</thead>
<tbody data-bind="foreach: listHelper.paginatedItems">
@ -673,7 +673,7 @@
</div>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Cancel</button>
<button class="btn btn-primary" data-bind="click: saveData">Save</button>
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Cancel') }}</button>
<button class="btn btn-primary" data-bind="click: saveData">{{ _('Save') }}</button>
</div>
</div>

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff