Add multilingual support, competition close/reopen, and backup directory

This commit is contained in:
Jan Meinl
2026-05-17 09:18:34 +02:00
parent bb9f3cd3eb
commit 777f11d93c
18 changed files with 1039 additions and 433 deletions
+171 -8
View File
@@ -2,9 +2,15 @@ package main
import (
"database/sql"
"encoding/csv"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
func registerCompetitionRoutes(mux *http.ServeMux) {
@@ -13,6 +19,8 @@ func registerCompetitionRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /api/competitions/{id}", requireAuth(handleGetCompetition))
mux.HandleFunc("PATCH /api/competitions/{id}", requireAuth(handleUpdateCompetition))
mux.HandleFunc("DELETE /api/competitions/{id}", requireAdmin(handleDeleteCompetition))
mux.HandleFunc("POST /api/competitions/{id}/close", requireAdmin(handleCloseCompetition))
mux.HandleFunc("POST /api/competitions/{id}/reopen", requireAdmin(handleReopenCompetition))
mux.HandleFunc("GET /api/competitions/{id}/members", requireAuth(handleListMembers))
mux.HandleFunc("POST /api/competitions/{id}/members", requireAuth(handleAddMember))
mux.HandleFunc("DELETE /api/competitions/{id}/members/{uid}", requireAuth(handleRemoveMember))
@@ -43,9 +51,9 @@ func handleListCompetitions(w http.ResponseWriter, r *http.Request) {
var rows *sql.Rows
var err error
if u.IsSystemAdmin {
rows, err = db.Query("SELECT id,name,allow_any_scorer_edit,rules_language,created_at FROM competitions ORDER BY created_at DESC")
rows, err = db.Query("SELECT id,name,allow_any_scorer_edit,rules_language,closed,closed_at,created_at FROM competitions ORDER BY created_at DESC")
} else {
rows, err = db.Query(`SELECT c.id,c.name,c.allow_any_scorer_edit,c.rules_language,c.created_at,cu.role
rows, err = db.Query(`SELECT c.id,c.name,c.allow_any_scorer_edit,c.rules_language,c.closed,c.closed_at,c.created_at,cu.role
FROM competitions c JOIN competition_users cu ON cu.competition_id=c.id
WHERE cu.user_id=? ORDER BY c.created_at DESC`, u.ID)
}
@@ -57,14 +65,15 @@ func handleListCompetitions(w http.ResponseWriter, r *http.Request) {
out := []Competition{}
for rows.Next() {
var c Competition
var allow int
var allow, closed int
if u.IsSystemAdmin {
rows.Scan(&c.ID, &c.Name, &allow, &c.RulesLanguage, &c.CreatedAt)
rows.Scan(&c.ID, &c.Name, &allow, &c.RulesLanguage, &closed, &c.ClosedAt, &c.CreatedAt)
c.Role = "system_admin"
} else {
rows.Scan(&c.ID, &c.Name, &allow, &c.RulesLanguage, &c.CreatedAt, &c.Role)
rows.Scan(&c.ID, &c.Name, &allow, &c.RulesLanguage, &closed, &c.ClosedAt, &c.CreatedAt, &c.Role)
}
c.AllowAnyScorerEdit = allow == 1
c.Closed = closed == 1
out = append(out, c)
}
writeJSON(w, http.StatusOK, out)
@@ -129,14 +138,15 @@ func handleGetCompetition(w http.ResponseWriter, r *http.Request) {
return
}
var c Competition
var allow int
err = db.QueryRow("SELECT id,name,allow_any_scorer_edit,rules_language,created_at FROM competitions WHERE id=?", id).
Scan(&c.ID, &c.Name, &allow, &c.RulesLanguage, &c.CreatedAt)
var allow, closed int
err = db.QueryRow("SELECT id,name,allow_any_scorer_edit,rules_language,closed,closed_at,created_at FROM competitions WHERE id=?", id).
Scan(&c.ID, &c.Name, &allow, &c.RulesLanguage, &closed, &c.ClosedAt, &c.CreatedAt)
if err != nil {
writeError(w, http.StatusNotFound, "not_found")
return
}
c.AllowAnyScorerEdit = allow == 1
c.Closed = closed == 1
c.Role = role
writeJSON(w, http.StatusOK, c)
}
@@ -153,6 +163,10 @@ func handleUpdateCompetition(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusForbidden, "forbidden")
return
}
if closed, _ := isCompetitionClosed(id); closed && role != "system_admin" {
writeError(w, http.StatusConflict, "competition_closed")
return
}
var req struct {
Name *string `json:"name"`
AllowAnyScorerEdit *bool `json:"allow_any_scorer_edit"`
@@ -189,6 +203,155 @@ func handleDeleteCompetition(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
func isCompetitionClosed(id int64) (bool, error) {
var closed int
err := db.QueryRow("SELECT closed FROM competitions WHERE id=?", id).Scan(&closed)
if err != nil {
return false, err
}
return closed == 1, nil
}
func handleCloseCompetition(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id")
return
}
var name, rulesLang string
if err := db.QueryRow("SELECT name,rules_language FROM competitions WHERE id=?", id).Scan(&name, &rulesLang); err != nil {
writeError(w, http.StatusNotFound, "not_found")
return
}
path, err := writeCompetitionBackup(id, name, rulesLang)
if err != nil {
writeError(w, http.StatusInternalServerError, "backup_error")
return
}
now := time.Now().UTC().Format(time.RFC3339)
if _, err := db.Exec("UPDATE competitions SET closed=1, closed_at=? WHERE id=?", now, id); err != nil {
writeError(w, http.StatusInternalServerError, "db_error")
return
}
hub.broadcast(id, "competition_updated", nil)
writeJSON(w, http.StatusOK, map[string]string{"backup": filepath.Base(path)})
}
func handleReopenCompetition(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid_id")
return
}
if _, err := db.Exec("UPDATE competitions SET closed=0, closed_at='' WHERE id=?", id); err != nil {
writeError(w, http.StatusInternalServerError, "db_error")
return
}
hub.broadcast(id, "competition_updated", nil)
w.WriteHeader(http.StatusNoContent)
}
// sanitizeFilename returns a safe filesystem fragment derived from name.
func sanitizeFilename(s string) string {
s = strings.TrimSpace(s)
var b strings.Builder
for _, r := range s {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
b.WriteRune(r)
case r == '-' || r == '_':
b.WriteRune(r)
case r == ' ':
b.WriteRune('_')
}
}
out := b.String()
if len(out) > 60 {
out = out[:60]
}
if out == "" {
out = "competition"
}
return out
}
// writeCompetitionBackup writes a CSV snapshot of all penalties (joined with
// pilot and current rule text) into the backup directory and returns the path.
func writeCompetitionBackup(id int64, name, rulesLang string) (string, error) {
dir := backupDir
if dir == "" {
dir = "backup"
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return "", err
}
rulesMu.RLock()
ruleMap := rules[rulesLang]
if ruleMap == nil {
ruleMap = rules["en"]
}
rulesMu.RUnlock()
rows, err := db.Query(`SELECT p.id,p.flight,p.date,p.pilot_number,
COALESCE(pl.last_name || ', ' || pl.first_name, ''),
p.rule_number,p.task,p.penalties_text,p.description,
COALESCE(u.display_name, u.username),p.transferred,p.created_at,p.updated_at
FROM penalties p
LEFT JOIN pilots pl ON pl.competition_id=p.competition_id AND pl.number=p.pilot_number
LEFT JOIN users u ON u.id=p.created_by
WHERE p.competition_id=? ORDER BY p.id`, id)
if err != nil {
return "", err
}
defer rows.Close()
ts := time.Now().UTC().Format("20060102T150405Z")
fname := fmt.Sprintf("competition_%d_%s_%s.csv", id, sanitizeFilename(name), ts)
path := filepath.Join(dir, fname)
f, err := os.Create(path)
if err != nil {
return "", err
}
defer f.Close()
cw := csv.NewWriter(f)
defer cw.Flush()
if err := cw.Write([]string{
"id", "flight", "date", "pilot_number", "pilot_name", "rule_number",
"rule_text", "suggested_penalty", "task", "penalties", "description",
"created_by", "transferred", "created_at", "updated_at",
}); err != nil {
return "", err
}
for rows.Next() {
var idv int64
var flight, date, pnum, pname, rnum, task, pens, desc, creator, createdAt, updatedAt string
var transferred int
if err := rows.Scan(&idv, &flight, &date, &pnum, &pname, &rnum, &task, &pens, &desc, &creator, &transferred, &createdAt, &updatedAt); err != nil {
return "", err
}
ruleText, suggested := "", ""
if ru, ok := ruleMap[rnum]; ok {
ruleText = ru.Text
suggested = ru.SuggestedPenalty
}
t := "0"
if transferred == 1 {
t = "1"
}
if err := cw.Write([]string{
strconv.FormatInt(idv, 10),
csvSafe(flight), csvSafe(date), csvSafe(pnum), csvSafe(pname),
csvSafe(rnum), csvSafe(ruleText), csvSafe(suggested),
csvSafe(task), csvSafe(pens), csvSafe(desc),
csvSafe(creator), t, createdAt, updatedAt,
}); err != nil {
return "", err
}
}
return path, nil
}
func handleListMembers(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
+2
View File
@@ -92,6 +92,8 @@ func migrate() error {
addColumns := []string{
`ALTER TABLE users ADD COLUMN must_change_password INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE competitions ADD COLUMN rules_language TEXT NOT NULL DEFAULT 'en'`,
`ALTER TABLE competitions ADD COLUMN closed INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE competitions ADD COLUMN closed_at TEXT NOT NULL DEFAULT ''`,
}
for _, s := range addColumns {
// Ignore "duplicate column" errors so the migration is idempotent.
+10
View File
@@ -21,18 +21,21 @@ type Config struct {
Addr string `json:"addr"`
DBPath string `json:"db_path"`
RulesDir string `json:"rules_dir"`
BackupDir string `json:"backup_dir"`
CORSOrigins []string `json:"cors_origins"`
CrossSiteCookies bool `json:"cross_site_cookies"`
}
var corsOrigins []string
var crossSiteCookies bool
var backupDir string
func defaultConfig() *Config {
return &Config{
Addr: ":8080",
DBPath: "penaltytracker.db",
RulesDir: "rules",
BackupDir: "backup",
CORSOrigins: []string{},
CrossSiteCookies: false,
}
@@ -78,6 +81,9 @@ func loadConfig(path string) (*Config, error) {
if cfg.RulesDir == "" {
cfg.RulesDir = "rules"
}
if cfg.BackupDir == "" {
cfg.BackupDir = "backup"
}
for i, o := range cfg.CORSOrigins {
cfg.CORSOrigins[i] = strings.TrimSpace(o)
}
@@ -95,6 +101,9 @@ func ensureDirectories(cfg *Config) error {
if err := ensureDir(cfg.RulesDir); err != nil {
return err
}
if err := ensureDir(cfg.BackupDir); err != nil {
return err
}
if dbDir := filepath.Dir(cfg.DBPath); dbDir != "" {
if err := ensureDir(dbDir); err != nil {
return err
@@ -118,6 +127,7 @@ func main() {
corsOrigins = cfg.CORSOrigins
crossSiteCookies = cfg.CrossSiteCookies
backupDir = cfg.BackupDir
if err := openDB(cfg.DBPath); err != nil {
log.Fatalf("db open: %v", err)
+2
View File
@@ -14,6 +14,8 @@ type Competition struct {
Name string `json:"name"`
AllowAnyScorerEdit bool `json:"allow_any_scorer_edit"`
RulesLanguage string `json:"rules_language"`
Closed bool `json:"closed"`
ClosedAt string `json:"closed_at,omitempty"`
CreatedAt string `json:"created_at"`
Role string `json:"role,omitempty"`
}
+16
View File
@@ -52,6 +52,10 @@ func handleApplyPenalties(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusForbidden, "forbidden")
return
}
if closed, _ := isCompetitionClosed(id); closed {
writeError(w, http.StatusConflict, "competition_closed")
return
}
var req struct {
Task string `json:"task"`
IDs []int64 `json:"ids"`
@@ -166,6 +170,10 @@ func handleCreatePenalty(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusForbidden, "forbidden")
return
}
if closed, _ := isCompetitionClosed(id); closed {
writeError(w, http.StatusConflict, "competition_closed")
return
}
var pen Penalty
if err := json.NewDecoder(r.Body).Decode(&pen); err != nil {
writeError(w, http.StatusBadRequest, "invalid_body")
@@ -239,6 +247,10 @@ func handleUpdatePenalty(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusForbidden, "forbidden")
return
}
if closed, _ := isCompetitionClosed(id); closed {
writeError(w, http.StatusConflict, "competition_closed")
return
}
var req struct {
Flight *string `json:"flight"`
Date *string `json:"date"`
@@ -330,6 +342,10 @@ func handleDeletePenalty(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusForbidden, "forbidden")
return
}
if closed, _ := isCompetitionClosed(id); closed {
writeError(w, http.StatusConflict, "competition_closed")
return
}
db.Exec("DELETE FROM penalties WHERE id=?", pid)
hub.broadcast(id, "penalty_deleted", map[string]int64{"id": pid})
w.WriteHeader(http.StatusNoContent)
+2
View File
@@ -37,6 +37,8 @@ const API = {
getCompetition: (id) => api("GET", `/api/competitions/${id}`),
updateCompetition: (id, b) => api("PATCH", `/api/competitions/${id}`, b),
deleteCompetition: (id) => api("DELETE", `/api/competitions/${id}`),
closeCompetition: (id) => api("POST", `/api/competitions/${id}/close`),
reopenCompetition: (id) => api("POST", `/api/competitions/${id}/reopen`),
listMembers: (id) => api("GET", `/api/competitions/${id}/members`),
addMember: (id, b) => api("POST", `/api/competitions/${id}/members`, b),
+6 -13
View File
@@ -55,6 +55,9 @@ function navigate(page, params) {
// onlyIfMustChange: true — redirect AWAY if user must NOT change
async function bootstrapAuth(options) {
options = options || {};
// Make sure English (fallback) and the browser-detected language are loaded
// before we attempt to render anything.
await setLang(detectInitialLang());
let user = null;
try {
user = await API.me();
@@ -66,7 +69,7 @@ async function bootstrapAuth(options) {
return null;
}
if (!user) return null;
setLang(user.language || CURRENT_LANG);
await setLang(user.language || CURRENT_LANG);
if (user.must_change_password && options.forbidIfMustChange) {
navigate("forcePassword");
return null;
@@ -86,7 +89,7 @@ function renderTopbar(user, opts) {
const lang = e.target.value;
try { await API.updateMe({ language: lang }); } catch (_) {}
user.language = lang;
setLang(lang);
await setLang(lang);
location.reload();
} },
...I18N_AVAILABLE.map((l) => el("option", { value: l, selected: l === user.language }, I18N_NAMES[l]))
@@ -150,7 +153,7 @@ function openProfileModal(user) {
const u = await API.updateMe(body);
if (u.language !== user.language) {
user.language = u.language;
setLang(u.language);
await setLang(u.language);
}
ok.textContent = t("saved");
ok.style.display = "block";
@@ -174,13 +177,3 @@ function queryParams() {
return out;
}
// Initialize language from saved/browser preference before authenticated state
// is known.
(function initInitialLang() {
const userLang = navigator.language ? navigator.language.slice(0, 2) : "en";
if (typeof I18N_AVAILABLE !== "undefined" && I18N_AVAILABLE.includes(userLang)) {
setLang(userLang);
} else {
setLang("en");
}
})();
+68 -7
View File
@@ -56,8 +56,15 @@
const r = state.competition.role;
return r === "system_admin" || r === "chief_scorer";
}
function isSysAdmin() {
return state.competition.role === "system_admin";
}
function isClosed() {
return !!state.competition.closed;
}
function canEditPenalty(p) {
if (isClosed()) return false;
if (isChief()) return true;
if (p.created_by === user.id) return true;
return !!state.competition.allow_any_scorer_edit;
@@ -139,11 +146,11 @@
);
const toolbar = el("div", { class: "toolbar" },
el("button", { class: "primary", onclick: () => openPenaltyModal() }, t("add_penalty")),
el("button", { class: "primary", disabled: isClosed(), onclick: () => { if (!isClosed()) openPenaltyModal(); } }, t("add_penalty")),
search,
applyFilter,
el("div", { class: "spacer" }),
isChief() && el("button", { onclick: openApplyByTaskModal }, t("apply_by_task")),
isChief() && !isClosed() && el("button", { onclick: openApplyByTaskModal }, t("apply_by_task")),
isChief() && el("button", { onclick: async () => {
try {
const blob = await API.exportPenaltiesCSV(competitionId);
@@ -1197,18 +1204,19 @@
// ---- Settings tab ------------------------------------------------------
function renderSettingsTab() {
const name = el("input", { type: "text", value: state.competition.name });
const allow = el("input", { type: "checkbox", checked: !!state.competition.allow_any_scorer_edit });
const readonly = isClosed() && !isSysAdmin();
const name = el("input", { type: "text", value: state.competition.name, disabled: readonly });
const allow = el("input", { type: "checkbox", checked: !!state.competition.allow_any_scorer_edit, disabled: readonly });
const currentRulesLang = state.competition.rules_language || "en";
const langOptions = (state.ruleLanguages && state.ruleLanguages.length)
? [...state.ruleLanguages].sort()
: I18N_AVAILABLE;
const rulesLang = el("select", null,
const rulesLang = el("select", { disabled: readonly },
...langOptions.map((l) => el("option", { value: l, selected: l === currentRulesLang },
I18N_NAMES[l] || l))
);
const msg = el("div", { class: "muted", style: { color: "var(--accent)", display: "none" } });
return el("div", { class: "card" },
const card = el("div", { class: "card" },
el("h2", null, t("settings_tab")),
el("div", { class: "field" }, el("label", null, t("competition_name")), name),
el("div", { class: "field" }, el("label", null, t("rules_language")), rulesLang,
@@ -1216,7 +1224,7 @@
el("label", { class: "row", style: { marginTop: "0.5rem" } }, allow, " " + t("allow_any_scorer_edit")),
msg,
el("div", { class: "row", style: { justifyContent: "flex-end", marginTop: "1rem" } },
el("button", { class: "primary", onclick: async () => {
el("button", { class: "primary", disabled: readonly, onclick: async () => {
try {
await API.updateCompetition(competitionId, {
name: name.value,
@@ -1232,6 +1240,57 @@
} }, t("save_settings")),
),
);
if (isSysAdmin()) {
const adminCard = el("div", { class: "card", style: { marginTop: "1rem" } });
adminCard.appendChild(el("h2", null, t("admin_zone")));
if (isClosed()) {
adminCard.appendChild(el("div", { class: "muted", style: { marginBottom: "0.5rem" } },
t("competition_closed_explain"),
state.competition.closed_at ? " (" + state.competition.closed_at + ")" : "",
));
adminCard.appendChild(el("div", { class: "row", style: { gap: "0.5rem", flexWrap: "wrap" } },
el("button", { class: "primary", onclick: async () => {
if (!confirm(t("confirm_reopen_competition"))) return;
try {
await API.reopenCompetition(competitionId);
state.competition = await API.getCompetition(competitionId);
render();
} catch (e) { alert((e.data && e.data.error) || e.message); }
} }, t("reopen_competition")),
el("button", { class: "danger", onclick: async () => {
if (!confirm(t("confirm_delete_competition"))) return;
try {
await API.deleteCompetition(competitionId);
navigate("competitions");
} catch (e) { alert((e.data && e.data.error) || e.message); }
} }, t("delete_competition")),
));
} else {
adminCard.appendChild(el("div", { class: "muted", style: { marginBottom: "0.5rem" } },
t("close_competition_explain")));
adminCard.appendChild(el("div", { class: "row", style: { gap: "0.5rem", flexWrap: "wrap" } },
el("button", { class: "primary", onclick: async () => {
if (!confirm(t("confirm_close_competition"))) return;
try {
const res = await API.closeCompetition(competitionId);
state.competition = await API.getCompetition(competitionId);
if (res && res.backup) alert(t("backup_written", { file: res.backup }));
render();
} catch (e) { alert((e.data && e.data.error) || e.message); }
} }, t("close_competition")),
el("button", { class: "danger", onclick: async () => {
if (!confirm(t("confirm_delete_competition"))) return;
try {
await API.deleteCompetition(competitionId);
navigate("competitions");
} catch (e) { alert((e.data && e.data.error) || e.message); }
} }, t("delete_competition")),
));
}
return el("div", null, card, adminCard);
}
return card;
}
// ---- Main render -------------------------------------------------------
@@ -1249,6 +1308,8 @@
state.competition.name,
" ",
el("span", { class: "badge accent" }, t(state.competition.role)),
isClosed() ? " " : null,
isClosed() ? el("span", { class: "badge warn" }, t("closed")) : null,
),
el("div", { class: "row" },
el("span", { class: "connection-status " + (state.wsOnline ? "online" : "offline") }),
+13 -1
View File
@@ -199,8 +199,20 @@
el("h2", null, c.name),
el("div", { class: "muted", style: { marginBottom: "0.5rem" } },
el("span", { class: "badge accent" }, t(c.role)),
c.closed ? " " : null,
c.closed ? el("span", { class: "badge warn" }, t("closed")) : null,
),
el("div", { class: "row", style: { gap: "0.5rem" } },
el("button", { class: "primary", onclick: () => navigate("competition", { id: c.id }) }, t("open")),
user.is_system_admin && el("button", { class: "danger", onclick: async () => {
if (!confirm(t("confirm_delete_competition_named", { name: c.name }))) return;
try {
await API.deleteCompetition(c.id);
await loadCompetitions();
render();
} catch (e) { alert((e.data && e.data.error) || e.message); }
} }, t("delete")),
),
el("button", { class: "primary", onclick: () => navigate("competition", { id: c.id }) }, t("open")),
));
}
container.appendChild(grid);
+39 -403
View File
@@ -1,419 +1,50 @@
// Lightweight i18n loader. Translation data lives in `i18n/<lang>.json`, one
// file per language so translators can edit each file independently.
const I18N_AVAILABLE = ["en", "de", "pl", "ru", "fr", "es", "pt"];
const I18N_NAMES = {
en: "English", de: "Deutsch", pl: "Polski", ru: "Русский",
fr: "Français", es: "Español", pt: "Português"
};
const I18N_BASE_PATH = "i18n";
const I18N_DATA = {
en: {
login_title: "Sign in",
username: "Username", password: "Password", sign_in: "Sign in",
invalid_credentials: "Invalid username or password",
logout: "Logout", settings: "Settings", language: "Language",
competitions: "Competitions", new_competition: "New competition",
competition_name: "Competition name", create: "Create", cancel: "Cancel",
role: "Role", system_admin: "System Admin", chief_scorer: "Chief Scorer", scorer: "Scorer",
pilots: "Pilots", penalties: "Penalties", members: "Members", rules: "Rules",
settings_tab: "Settings",
number: "Number", last_name: "Last name", first_name: "First name",
country: "Country", balloon_id: "Registration",
rules_language: "Rules language",
rules_language_hint: "Language of rule texts loaded for this competition (UI language stays per user)",
add_pilot: "Add pilot", import_csv: "Import CSV", export_csv: "Export CSV",
flight: "Flight", date: "Date", pilot_number: "Pilot No.",
pilot_name: "Pilot name", rule: "Rule", task: "Task",
penalty_values: "Penalties", description: "Description", created_by: "Created by",
transferred: "Transferred", actions: "Actions",
add_penalty: "Add penalty", edit: "Edit", delete: "Delete", save: "Save",
confirm_delete: "Delete this entry?", search_rule: "Search rule by number or text",
suggested_penalty: "Suggested penalty",
escalation: "Escalation behavior",
escalation_same: "Stays the same", escalation_doubled: "Doubled each time",
escalation_escalate: "Escalates: ",
add_member: "Add member", remove: "Remove",
add_user: "Add user", users: "Users",
display_name: "Display name", is_admin: "Admin",
allow_any_scorer_edit: "Allow any scorer to edit penalties",
open: "Open", back: "Back", change_password: "Change password",
new_password: "New password", csv_paste: "Paste CSV (number,last,first,country,registration)",
no_pilots: "No pilots yet", no_penalties: "No penalties yet",
no_members: "No members yet", no_competitions: "No competitions",
select_pilot: "Select pilot", rule_number_short: "Rule No.",
transferred_only: "Only untransferred",
showing_n_of_m: "Showing {n} of {m}",
online: "Online", offline: "Offline",
forbidden: "Not allowed",
save_settings: "Save settings",
saved: "Saved",
yes: "Yes", no: "No",
backend_url: "Backend URL",
backend_url_hint: "Leave empty to use the same origin (e.g. http://192.168.0.10:8080)",
profile: "Profile",
current_password: "Current password",
leave_blank_keep: "Leave blank to keep current",
username_taken: "Username already taken",
prior_penalties: "Prior penalties for this pilot and rule",
none: "None",
applied: "Applied",
apply_by_task: "Apply by task",
apply_by_task_explain: "Confirm all open penalties for a task at once. Penalties are only marked applied after confirmation.",
apply_n_open: "Apply {n} open",
confirm_apply_task: "Mark all {n} open penalties for task '{task}' as applied?",
summary: "Summary",
penalty_summary: "Penalty summary",
rule: "Rule",
rule_not_found: "rule not found",
close: "Close",
count: "#",
count_hint: "Number of prior penalties for this pilot and rule",
prior_count: "Prior count (this pilot & rule)",
search_penalties: "Search penalties…",
filter_all: "All",
filter_open: "Open only",
filter_applied: "Applied only",
total: "Total", open: "Open",
repeat_password: "Repeat password",
password_too_short: "Password must be at least 8 characters",
passwords_dont_match: "Passwords do not match",
too_many_attempts: "Too many login attempts — please wait a few minutes",
profile_username_readonly: "Username can only be changed by a system administrator",
profile_displayname_readonly: "Display name can only be changed by a system administrator",
must_change_password: "Must change password",
confirm_force_password: "Force this user to change their password on next request?",
force_password_change: "Force password change",
force_password_explain: "An administrator has required you to set a new password before you can continue.",
user_not_found: "User not found",
show_incidents: "Show incidents",
hide_incidents: "Hide incidents",
apply_select_task: "Pick a task to start applying its open penalties.",
no_open_penalties: "No open penalties to apply.",
no_task: "(no task)",
start_apply: "Start",
step_x_of_y: "Pilot {x} of {y}",
pilot: "Pilot",
next: "Next",
to_overview: "To overview",
apply_overview: "Overview",
apply_overview_explain: "{n} penalty/penalties will be marked applied on save.",
nothing_to_apply: "Nothing to apply.",
confirm_save_partial: "Save and apply {n} penalty/penalties reviewed so far?",
},
de: {
login_title: "Anmelden",
username: "Benutzername", password: "Passwort", sign_in: "Anmelden",
invalid_credentials: "Falscher Benutzername oder Passwort",
logout: "Abmelden", settings: "Einstellungen", language: "Sprache",
competitions: "Wettbewerbe", new_competition: "Neuer Wettbewerb",
competition_name: "Wettbewerbsname", create: "Erstellen", cancel: "Abbrechen",
role: "Rolle", system_admin: "Systemadmin", chief_scorer: "Chief-Scorer", scorer: "Scorer",
pilots: "Piloten", penalties: "Strafen", members: "Mitglieder", rules: "Regeln",
settings_tab: "Einstellungen",
number: "Nummer", last_name: "Nachname", first_name: "Vorname",
country: "Land", balloon_id: "Kennung",
rules_language: "Sprache der Regeln",
rules_language_hint: "Sprache der Regeltexte für diesen Wettbewerb (Bedienoberfläche bleibt pro Benutzer)",
add_pilot: "Pilot hinzufügen", import_csv: "CSV importieren", export_csv: "CSV exportieren",
flight: "Fahrt", date: "Datum", pilot_number: "Pilot-Nr.",
pilot_name: "Pilotenname", rule: "Regel", task: "Aufgabe",
penalty_values: "Strafen", description: "Beschreibung", created_by: "Angelegt von",
transferred: "Übertragen", actions: "Aktionen",
add_penalty: "Strafe hinzufügen", edit: "Bearbeiten", delete: "Löschen", save: "Speichern",
confirm_delete: "Eintrag löschen?", search_rule: "Regel nach Nummer oder Text suchen",
suggested_penalty: "Vorgeschlagene Strafe",
escalation: "Verhalten bei Wiederholung",
escalation_same: "Bleibt gleich", escalation_doubled: "Wird jedes Mal verdoppelt",
escalation_escalate: "Höherstufung: ",
add_member: "Mitglied hinzufügen", remove: "Entfernen",
add_user: "Benutzer anlegen", users: "Benutzer",
display_name: "Anzeigename", is_admin: "Admin",
allow_any_scorer_edit: "Alle Scorer dürfen Strafen bearbeiten",
open: "Öffnen", back: "Zurück", change_password: "Passwort ändern",
new_password: "Neues Passwort", csv_paste: "CSV einfügen (Nr,Nachname,Vorname,Land,Kennung)",
no_pilots: "Noch keine Piloten", no_penalties: "Noch keine Strafen",
no_members: "Noch keine Mitglieder", no_competitions: "Keine Wettbewerbe",
select_pilot: "Pilot wählen", rule_number_short: "Regel-Nr.",
transferred_only: "Nur nicht übertragene",
showing_n_of_m: "{n} von {m}",
online: "Online", offline: "Offline",
forbidden: "Keine Berechtigung",
save_settings: "Einstellungen speichern",
saved: "Gespeichert",
yes: "Ja", no: "Nein",
backend_url: "Backend-URL",
backend_url_hint: "Leer lassen für gleichen Ursprung (z.B. http://192.168.0.10:8080)",
profile: "Profil",
current_password: "Aktuelles Passwort",
leave_blank_keep: "Leer lassen um beizubehalten",
username_taken: "Benutzername bereits vergeben",
prior_penalties: "Frühere Strafen für diesen Piloten und diese Regel",
none: "Keine",
applied: "Angewendet",
apply_by_task: "Pro Task anwenden",
apply_by_task_explain: "Bestätige alle offenen Strafen einer Aufgabe gemeinsam. Erst nach Bestätigung gelten die Strafen als angewendet.",
apply_n_open: "{n} offene anwenden",
confirm_apply_task: "Alle {n} offenen Strafen der Aufgabe '{task}' als angewendet markieren?",
summary: "Übersicht",
penalty_summary: "Strafen-Übersicht",
rule: "Regel",
rule_not_found: "Regel nicht gefunden",
close: "Schließen",
count: "#",
count_hint: "Anzahl früherer Strafen für diesen Piloten und diese Regel",
prior_count: "Frühere Anzahl (dieser Pilot & Regel)",
search_penalties: "Strafen durchsuchen…",
filter_all: "Alle",
filter_open: "Nur offene",
filter_applied: "Nur angewendete",
total: "Gesamt", open: "Offen",
repeat_password: "Passwort wiederholen",
password_too_short: "Passwort muss mindestens 8 Zeichen lang sein",
passwords_dont_match: "Passwörter stimmen nicht überein",
too_many_attempts: "Zu viele Anmeldeversuche — bitte ein paar Minuten warten",
profile_username_readonly: "Der Benutzername kann nur vom Systemadministrator geändert werden",
profile_displayname_readonly: "Der Anzeigename kann nur vom Systemadministrator geändert werden",
must_change_password: "Passwortwechsel erforderlich",
confirm_force_password: "Diesen Benutzer beim nächsten Zugriff zum Passwortwechsel zwingen?",
force_password_change: "Passwortwechsel erzwingen",
force_password_explain: "Ein Administrator hat festgelegt, dass du ein neues Passwort vergeben musst, bevor du fortfahren kannst.",
user_not_found: "Benutzer nicht gefunden",
show_incidents: "Vorfälle anzeigen",
hide_incidents: "Vorfälle ausblenden",
apply_select_task: "Aufgabe wählen, deren offene Strafen angewendet werden sollen.",
no_open_penalties: "Keine offenen Strafen zum Anwenden.",
no_task: "(ohne Aufgabe)",
start_apply: "Start",
step_x_of_y: "Pilot {x} von {y}",
pilot: "Pilot",
next: "Weiter",
to_overview: "Zur Übersicht",
apply_overview: "Übersicht",
apply_overview_explain: "Beim Speichern werden {n} Strafe(n) als angewendet markiert.",
nothing_to_apply: "Nichts anzuwenden.",
confirm_save_partial: "{n} bisher überprüfte Strafe(n) jetzt speichern und anwenden?",
},
pl: {
login_title: "Zaloguj się",
username: "Nazwa użytkownika", password: "Hasło", sign_in: "Zaloguj",
invalid_credentials: "Nieprawidłowy login lub hasło",
logout: "Wyloguj", settings: "Ustawienia", language: "Język",
competitions: "Zawody", new_competition: "Nowe zawody",
competition_name: "Nazwa zawodów", create: "Utwórz", cancel: "Anuluj",
role: "Rola", system_admin: "Administrator", chief_scorer: "Chief-Scorer", scorer: "Scorer",
pilots: "Piloci", penalties: "Kary", members: "Członkowie", rules: "Zasady",
settings_tab: "Ustawienia",
number: "Numer", last_name: "Nazwisko", first_name: "Imię",
country: "Kraj", balloon_id: "Registration",
rules_language: "Język zasad",
rules_language_hint: "Język tekstu zasad dla tych zawodów (język interfejsu pozostaje per użytkownik)",
add_pilot: "Dodaj pilota", import_csv: "Import CSV", export_csv: "Eksport CSV",
flight: "Lot", date: "Data", pilot_number: "Nr pilota",
pilot_name: "Imię i nazwisko", rule: "Zasada", task: "Zadanie",
penalty_values: "Kary", description: "Opis", created_by: "Wprowadził",
transferred: "Przesłano", actions: "Akcje",
add_penalty: "Dodaj karę", edit: "Edytuj", delete: "Usuń", save: "Zapisz",
confirm_delete: "Usunąć ten wpis?", search_rule: "Szukaj zasady po numerze lub tekście",
suggested_penalty: "Sugerowana kara",
escalation: "Zachowanie przy powtórzeniu",
escalation_same: "Bez zmian", escalation_doubled: "Podwajana za każdym razem",
escalation_escalate: "Eskalacja: ",
add_member: "Dodaj członka", remove: "Usuń",
add_user: "Dodaj użytkownika", users: "Użytkownicy",
display_name: "Wyświetlana nazwa", is_admin: "Admin",
allow_any_scorer_edit: "Pozwól dowolnemu scorerowi edytować kary",
open: "Otwórz", back: "Wstecz", change_password: "Zmień hasło",
new_password: "Nowe hasło", csv_paste: "Wklej CSV (nr,nazwisko,imię,kraj,registration)",
no_pilots: "Brak pilotów", no_penalties: "Brak kar",
no_members: "Brak członków", no_competitions: "Brak zawodów",
select_pilot: "Wybierz pilota", rule_number_short: "Nr zasady",
transferred_only: "Tylko nieprzesłane",
showing_n_of_m: "{n} z {m}",
online: "Online", offline: "Offline",
forbidden: "Brak uprawnień",
save_settings: "Zapisz ustawienia",
saved: "Zapisano",
yes: "Tak", no: "Nie",
},
ru: {
login_title: "Вход",
username: "Имя пользователя", password: "Пароль", sign_in: "Войти",
invalid_credentials: "Неверное имя или пароль",
logout: "Выйти", settings: "Настройки", language: "Язык",
competitions: "Соревнования", new_competition: "Новое соревнование",
competition_name: "Название", create: "Создать", cancel: "Отмена",
role: "Роль", system_admin: "Администратор", chief_scorer: "Chief-Scorer", scorer: "Scorer",
pilots: "Пилоты", penalties: "Штрафы", members: "Участники", rules: "Правила",
settings_tab: "Настройки",
number: "Номер", last_name: "Фамилия", first_name: "Имя",
country: "Страна", balloon_id: "Registration",
rules_language: "Язык правил",
rules_language_hint: "Язык текстов правил для этого соревнования (язык интерфейса — индивидуальный)",
add_pilot: "Добавить пилота", import_csv: "Импорт CSV", export_csv: "Экспорт CSV",
flight: "Полёт", date: "Дата", pilot_number: "№ пилота",
pilot_name: "Имя пилота", rule: "Правило", task: "Задание",
penalty_values: "Штрафы", description: "Описание", created_by: "Автор",
transferred: "Передано", actions: "Действия",
add_penalty: "Добавить штраф", edit: "Редактировать", delete: "Удалить", save: "Сохранить",
confirm_delete: "Удалить запись?", search_rule: "Поиск правила по номеру или тексту",
suggested_penalty: "Рекомендованный штраф",
escalation: "Поведение при повторе",
escalation_same: "Без изменений", escalation_doubled: "Удваивается каждый раз",
escalation_escalate: "Эскалация: ",
add_member: "Добавить участника", remove: "Удалить",
add_user: "Создать пользователя", users: "Пользователи",
display_name: "Отображаемое имя", is_admin: "Админ",
allow_any_scorer_edit: "Любой Scorer может редактировать штрафы",
open: "Открыть", back: "Назад", change_password: "Изменить пароль",
new_password: "Новый пароль", csv_paste: "Вставьте CSV (№,фамилия,имя,страна,registration)",
no_pilots: "Нет пилотов", no_penalties: "Нет штрафов",
no_members: "Нет участников", no_competitions: "Нет соревнований",
select_pilot: "Выберите пилота", rule_number_short: "№ правила",
transferred_only: "Только непереданные",
showing_n_of_m: "{n} из {m}",
online: "Онлайн", offline: "Оффлайн",
forbidden: "Нет доступа",
save_settings: "Сохранить настройки",
saved: "Сохранено",
yes: "Да", no: "Нет",
},
fr: {
login_title: "Connexion",
username: "Nom d'utilisateur", password: "Mot de passe", sign_in: "Connexion",
invalid_credentials: "Identifiants invalides",
logout: "Déconnexion", settings: "Paramètres", language: "Langue",
competitions: "Compétitions", new_competition: "Nouvelle compétition",
competition_name: "Nom", create: "Créer", cancel: "Annuler",
role: "Rôle", system_admin: "Administrateur", chief_scorer: "Chief-Scorer", scorer: "Scorer",
pilots: "Pilotes", penalties: "Pénalités", members: "Membres", rules: "Règles",
settings_tab: "Paramètres",
number: "Numéro", last_name: "Nom", first_name: "Prénom",
country: "Pays", balloon_id: "Registration",
rules_language: "Langue des règles",
rules_language_hint: "Langue des textes de règles pour cette compétition (la langue de l'interface reste par utilisateur)",
add_pilot: "Ajouter un pilote", import_csv: "Importer CSV", export_csv: "Exporter CSV",
flight: "Vol", date: "Date", pilot_number: "N° pilote",
pilot_name: "Nom du pilote", rule: "Règle", task: "Épreuve",
penalty_values: "Pénalités", description: "Description", created_by: "Créé par",
transferred: "Transféré", actions: "Actions",
add_penalty: "Ajouter pénalité", edit: "Modifier", delete: "Supprimer", save: "Enregistrer",
confirm_delete: "Supprimer cette entrée ?", search_rule: "Rechercher une règle par numéro ou texte",
suggested_penalty: "Pénalité suggérée",
escalation: "Comportement en cas de répétition",
escalation_same: "Reste identique", escalation_doubled: "Doublée à chaque fois",
escalation_escalate: "Escalade : ",
add_member: "Ajouter membre", remove: "Retirer",
add_user: "Créer un utilisateur", users: "Utilisateurs",
display_name: "Nom affiché", is_admin: "Admin",
allow_any_scorer_edit: "Tous les scorers peuvent modifier les pénalités",
open: "Ouvrir", back: "Retour", change_password: "Changer le mot de passe",
new_password: "Nouveau mot de passe", csv_paste: "Coller CSV (n°,nom,prénom,pays,registration)",
no_pilots: "Aucun pilote", no_penalties: "Aucune pénalité",
no_members: "Aucun membre", no_competitions: "Aucune compétition",
select_pilot: "Choisir un pilote", rule_number_short: "N° règle",
transferred_only: "Non transférés uniquement",
showing_n_of_m: "{n} sur {m}",
online: "En ligne", offline: "Hors ligne",
forbidden: "Non autorisé",
save_settings: "Enregistrer",
saved: "Enregistré",
yes: "Oui", no: "Non",
},
es: {
login_title: "Iniciar sesión",
username: "Usuario", password: "Contraseña", sign_in: "Entrar",
invalid_credentials: "Usuario o contraseña incorrectos",
logout: "Salir", settings: "Ajustes", language: "Idioma",
competitions: "Competiciones", new_competition: "Nueva competición",
competition_name: "Nombre", create: "Crear", cancel: "Cancelar",
role: "Rol", system_admin: "Administrador", chief_scorer: "Chief-Scorer", scorer: "Scorer",
pilots: "Pilotos", penalties: "Penalizaciones", members: "Miembros", rules: "Reglas",
settings_tab: "Ajustes",
number: "Número", last_name: "Apellido", first_name: "Nombre",
country: "País", balloon_id: "Registration",
rules_language: "Idioma de las reglas",
rules_language_hint: "Idioma de los textos de reglas para esta competición (el idioma de la interfaz se mantiene por usuario)",
add_pilot: "Añadir piloto", import_csv: "Importar CSV", export_csv: "Exportar CSV",
flight: "Vuelo", date: "Fecha", pilot_number: "N.º piloto",
pilot_name: "Nombre piloto", rule: "Regla", task: "Tarea",
penalty_values: "Penalizaciones", description: "Descripción", created_by: "Creado por",
transferred: "Transferido", actions: "Acciones",
add_penalty: "Añadir penalización", edit: "Editar", delete: "Eliminar", save: "Guardar",
confirm_delete: "¿Eliminar este registro?", search_rule: "Buscar regla por número o texto",
suggested_penalty: "Penalización sugerida",
escalation: "Comportamiento al repetirse",
escalation_same: "Sin cambios", escalation_doubled: "Se duplica cada vez",
escalation_escalate: "Escala: ",
add_member: "Añadir miembro", remove: "Quitar",
add_user: "Crear usuario", users: "Usuarios",
display_name: "Nombre mostrado", is_admin: "Admin",
allow_any_scorer_edit: "Cualquier scorer puede editar penalizaciones",
open: "Abrir", back: "Atrás", change_password: "Cambiar contraseña",
new_password: "Nueva contraseña", csv_paste: "Pegar CSV (n.º,apellido,nombre,país,registration)",
no_pilots: "Sin pilotos", no_penalties: "Sin penalizaciones",
no_members: "Sin miembros", no_competitions: "Sin competiciones",
select_pilot: "Elegir piloto", rule_number_short: "N.º regla",
transferred_only: "Solo no transferidas",
showing_n_of_m: "{n} de {m}",
online: "En línea", offline: "Sin conexión",
forbidden: "No permitido",
save_settings: "Guardar ajustes",
saved: "Guardado",
yes: "Sí", no: "No",
},
pt: {
login_title: "Entrar",
username: "Utilizador", password: "Palavra-passe", sign_in: "Entrar",
invalid_credentials: "Utilizador ou palavra-passe inválidos",
logout: "Sair", settings: "Definições", language: "Idioma",
competitions: "Competições", new_competition: "Nova competição",
competition_name: "Nome", create: "Criar", cancel: "Cancelar",
role: "Papel", system_admin: "Administrador", chief_scorer: "Chief-Scorer", scorer: "Scorer",
pilots: "Pilotos", penalties: "Penalizações", members: "Membros", rules: "Regras",
settings_tab: "Definições",
number: "Número", last_name: "Apelido", first_name: "Nome",
country: "País", balloon_id: "Registration",
rules_language: "Idioma das regras",
rules_language_hint: "Idioma dos textos das regras desta competição (o idioma da interface mantém-se por utilizador)",
add_pilot: "Adicionar piloto", import_csv: "Importar CSV", export_csv: "Exportar CSV",
flight: "Voo", date: "Data", pilot_number: "N.º piloto",
pilot_name: "Nome do piloto", rule: "Regra", task: "Tarefa",
penalty_values: "Penalizações", description: "Descrição", created_by: "Criado por",
transferred: "Transferido", actions: "Ações",
add_penalty: "Adicionar penalização", edit: "Editar", delete: "Eliminar", save: "Guardar",
confirm_delete: "Eliminar este registo?", search_rule: "Procurar regra por número ou texto",
suggested_penalty: "Penalização sugerida",
escalation: "Comportamento em caso de repetição",
escalation_same: "Sem alteração", escalation_doubled: "Duplicada de cada vez",
escalation_escalate: "Escala: ",
add_member: "Adicionar membro", remove: "Remover",
add_user: "Criar utilizador", users: "Utilizadores",
display_name: "Nome mostrado", is_admin: "Admin",
allow_any_scorer_edit: "Qualquer scorer pode editar penalizações",
open: "Abrir", back: "Voltar", change_password: "Alterar palavra-passe",
new_password: "Nova palavra-passe", csv_paste: "Colar CSV (n.º,apelido,nome,país,registration)",
no_pilots: "Sem pilotos", no_penalties: "Sem penalizações",
no_members: "Sem membros", no_competitions: "Sem competições",
select_pilot: "Escolher piloto", rule_number_short: "N.º regra",
transferred_only: "Apenas não transferidas",
showing_n_of_m: "{n} de {m}",
online: "Online", offline: "Offline",
forbidden: "Não autorizado",
save_settings: "Guardar definições",
saved: "Guardado",
yes: "Sim", no: "Não",
},
};
const I18N_DATA = {};
const I18N_PENDING = {};
let CURRENT_LANG = "en";
function setLang(lang) {
if (!I18N_DATA[lang]) lang = "en";
function i18nURL(lang) {
return `${I18N_BASE_PATH}/${lang}.json`;
}
async function loadLang(lang) {
if (!I18N_AVAILABLE.includes(lang)) lang = "en";
if (I18N_DATA[lang]) return I18N_DATA[lang];
if (I18N_PENDING[lang]) return I18N_PENDING[lang];
const p = fetch(i18nURL(lang), { cache: "no-cache" })
.then((r) => r.ok ? r.json() : Promise.reject(new Error("i18n_load_failed")))
.then((data) => { I18N_DATA[lang] = data; delete I18N_PENDING[lang]; return data; })
.catch((err) => { delete I18N_PENDING[lang]; throw err; });
I18N_PENDING[lang] = p;
return p;
}
async function setLang(lang) {
if (!I18N_AVAILABLE.includes(lang)) lang = "en";
// English is the fallback for missing keys, so make sure it is always loaded.
if (!I18N_DATA.en) {
try { await loadLang("en"); } catch (e) {}
}
if (lang !== "en") {
try { await loadLang(lang); } catch (e) {}
}
CURRENT_LANG = lang;
document.documentElement.lang = lang;
}
function t(key, vars) {
const d = I18N_DATA[CURRENT_LANG] || I18N_DATA.en;
let str = d[key] || I18N_DATA.en[key] || key;
const d = I18N_DATA[CURRENT_LANG] || I18N_DATA.en || {};
const fallback = I18N_DATA.en || {};
let str = d[key] || fallback[key] || key;
if (vars) {
for (const k in vars) {
str = str.replaceAll("{" + k + "}", vars[k]);
@@ -421,3 +52,8 @@ function t(key, vars) {
}
return str;
}
function detectInitialLang() {
const navLang = (navigator.language || "en").slice(0, 2);
return I18N_AVAILABLE.includes(navLang) ? navLang : "en";
}
+146
View File
@@ -0,0 +1,146 @@
{
"login_title": "Anmelden",
"username": "Benutzername",
"password": "Passwort",
"sign_in": "Anmelden",
"invalid_credentials": "Falscher Benutzername oder Passwort",
"logout": "Abmelden",
"settings": "Einstellungen",
"language": "Sprache",
"competitions": "Wettbewerbe",
"new_competition": "Neuer Wettbewerb",
"competition_name": "Wettbewerbsname",
"create": "Erstellen",
"cancel": "Abbrechen",
"role": "Rolle",
"system_admin": "Systemadmin",
"chief_scorer": "Chief-Scorer",
"scorer": "Scorer",
"pilots": "Piloten",
"penalties": "Strafen",
"members": "Mitglieder",
"rules": "Regeln",
"settings_tab": "Einstellungen",
"number": "Nummer",
"last_name": "Nachname",
"first_name": "Vorname",
"country": "Land",
"balloon_id": "Kennung",
"rules_language": "Sprache der Regeln",
"rules_language_hint": "Sprache der Regeltexte für diesen Wettbewerb (Bedienoberfläche bleibt pro Benutzer)",
"add_pilot": "Pilot hinzufügen",
"import_csv": "CSV importieren",
"export_csv": "CSV exportieren",
"flight": "Fahrt",
"date": "Datum",
"pilot_number": "Pilot-Nr.",
"pilot_name": "Pilotenname",
"rule": "Regel",
"task": "Aufgabe",
"penalty_values": "Strafen",
"description": "Beschreibung",
"created_by": "Angelegt von",
"transferred": "Übertragen",
"actions": "Aktionen",
"add_penalty": "Strafe hinzufügen",
"edit": "Bearbeiten",
"delete": "Löschen",
"save": "Speichern",
"confirm_delete": "Eintrag löschen?",
"search_rule": "Regel nach Nummer oder Text suchen",
"suggested_penalty": "Vorgeschlagene Strafe",
"escalation": "Verhalten bei Wiederholung",
"escalation_same": "Bleibt gleich",
"escalation_doubled": "Wird jedes Mal verdoppelt",
"escalation_escalate": "Höherstufung: ",
"add_member": "Mitglied hinzufügen",
"remove": "Entfernen",
"add_user": "Benutzer anlegen",
"users": "Benutzer",
"display_name": "Anzeigename",
"is_admin": "Admin",
"allow_any_scorer_edit": "Alle Scorer dürfen Strafen bearbeiten",
"open": "Öffnen",
"back": "Zurück",
"change_password": "Passwort ändern",
"new_password": "Neues Passwort",
"csv_paste": "CSV einfügen (Nr,Nachname,Vorname,Land,Kennung)",
"no_pilots": "Noch keine Piloten",
"no_penalties": "Noch keine Strafen",
"no_members": "Noch keine Mitglieder",
"no_competitions": "Keine Wettbewerbe",
"select_pilot": "Pilot wählen",
"rule_number_short": "Regel-Nr.",
"transferred_only": "Nur nicht übertragene",
"showing_n_of_m": "{n} von {m}",
"online": "Online",
"offline": "Offline",
"forbidden": "Keine Berechtigung",
"save_settings": "Einstellungen speichern",
"saved": "Gespeichert",
"yes": "Ja",
"no": "Nein",
"backend_url": "Backend-URL",
"backend_url_hint": "Leer lassen für gleichen Ursprung (z.B. http://192.168.0.10:8080)",
"profile": "Profil",
"current_password": "Aktuelles Passwort",
"leave_blank_keep": "Leer lassen um beizubehalten",
"username_taken": "Benutzername bereits vergeben",
"prior_penalties": "Frühere Strafen für diesen Piloten und diese Regel",
"none": "Keine",
"applied": "Angewendet",
"apply_by_task": "Pro Task anwenden",
"apply_by_task_explain": "Bestätige alle offenen Strafen einer Aufgabe gemeinsam. Erst nach Bestätigung gelten die Strafen als angewendet.",
"apply_n_open": "{n} offene anwenden",
"confirm_apply_task": "Alle {n} offenen Strafen der Aufgabe '{task}' als angewendet markieren?",
"summary": "Übersicht",
"penalty_summary": "Strafen-Übersicht",
"rule_not_found": "Regel nicht gefunden",
"close": "Schließen",
"count": "#",
"count_hint": "Anzahl früherer Strafen für diesen Piloten und diese Regel",
"prior_count": "Frühere Anzahl (dieser Pilot & Regel)",
"search_penalties": "Strafen durchsuchen…",
"filter_all": "Alle",
"filter_open": "Nur offene",
"filter_applied": "Nur angewendete",
"total": "Gesamt",
"repeat_password": "Passwort wiederholen",
"password_too_short": "Passwort muss mindestens 8 Zeichen lang sein",
"passwords_dont_match": "Passwörter stimmen nicht überein",
"too_many_attempts": "Zu viele Anmeldeversuche — bitte ein paar Minuten warten",
"profile_username_readonly": "Der Benutzername kann nur vom Systemadministrator geändert werden",
"profile_displayname_readonly": "Der Anzeigename kann nur vom Systemadministrator geändert werden",
"must_change_password": "Passwortwechsel erforderlich",
"confirm_force_password": "Diesen Benutzer beim nächsten Zugriff zum Passwortwechsel zwingen?",
"force_password_change": "Passwortwechsel erzwingen",
"force_password_explain": "Ein Administrator hat festgelegt, dass du ein neues Passwort vergeben musst, bevor du fortfahren kannst.",
"user_not_found": "Benutzer nicht gefunden",
"show_incidents": "Vorfälle anzeigen",
"hide_incidents": "Vorfälle ausblenden",
"apply_select_task": "Aufgabe wählen, deren offene Strafen angewendet werden sollen.",
"no_open_penalties": "Keine offenen Strafen zum Anwenden.",
"no_task": "(ohne Aufgabe)",
"start_apply": "Start",
"step_x_of_y": "Pilot {x} von {y}",
"pilot": "Pilot",
"next": "Weiter",
"to_overview": "Zur Übersicht",
"apply_overview": "Übersicht",
"apply_overview_explain": "Beim Speichern werden {n} Strafe(n) als angewendet markiert.",
"nothing_to_apply": "Nichts anzuwenden.",
"confirm_save_partial": "{n} bisher überprüfte Strafe(n) jetzt speichern und anwenden?",
"closed": "Beendet",
"admin_zone": "Verwaltung",
"close_competition": "Wettbewerb beenden",
"reopen_competition": "Wettbewerb wieder öffnen",
"delete_competition": "Wettbewerb löschen",
"close_competition_explain": "Beim Beenden werden alle Strafen gesperrt und eine CSV-Sicherung mit den aktuellen Regeltexten erstellt. Ein Systemadministrator kann den Wettbewerb später wieder öffnen.",
"competition_closed_explain": "Dieser Wettbewerb ist beendet. Strafen können erst geändert werden, wenn er wieder geöffnet wird.",
"confirm_close_competition": "Diesen Wettbewerb wirklich beenden? Strafen werden gesperrt und eine CSV-Sicherung wird geschrieben.",
"confirm_reopen_competition": "Diesen Wettbewerb wieder öffnen? Strafen werden erneut bearbeitbar.",
"confirm_delete_competition": "Diesen Wettbewerb löschen? Alle Piloten, Strafen und Mitglieder werden unwiderruflich entfernt.",
"confirm_delete_competition_named": "Wettbewerb '{name}' löschen? Alle Piloten, Strafen und Mitglieder werden unwiderruflich entfernt.",
"backup_written": "Sicherung erstellt: {file}",
"competition_closed": "Wettbewerb ist beendet"
}
+146
View File
@@ -0,0 +1,146 @@
{
"login_title": "Sign in",
"username": "Username",
"password": "Password",
"sign_in": "Sign in",
"invalid_credentials": "Invalid username or password",
"logout": "Logout",
"settings": "Settings",
"language": "Language",
"competitions": "Competitions",
"new_competition": "New competition",
"competition_name": "Competition name",
"create": "Create",
"cancel": "Cancel",
"role": "Role",
"system_admin": "System Admin",
"chief_scorer": "Chief Scorer",
"scorer": "Scorer",
"pilots": "Pilots",
"penalties": "Penalties",
"members": "Members",
"rules": "Rules",
"settings_tab": "Settings",
"number": "Number",
"last_name": "Last name",
"first_name": "First name",
"country": "Country",
"balloon_id": "Registration",
"rules_language": "Rules language",
"rules_language_hint": "Language of rule texts loaded for this competition (UI language stays per user)",
"add_pilot": "Add pilot",
"import_csv": "Import CSV",
"export_csv": "Export CSV",
"flight": "Flight",
"date": "Date",
"pilot_number": "Pilot No.",
"pilot_name": "Pilot name",
"rule": "Rule",
"task": "Task",
"penalty_values": "Penalties",
"description": "Description",
"created_by": "Created by",
"transferred": "Transferred",
"actions": "Actions",
"add_penalty": "Add penalty",
"edit": "Edit",
"delete": "Delete",
"save": "Save",
"confirm_delete": "Delete this entry?",
"search_rule": "Search rule by number or text",
"suggested_penalty": "Suggested penalty",
"escalation": "Escalation behavior",
"escalation_same": "Stays the same",
"escalation_doubled": "Doubled each time",
"escalation_escalate": "Escalates: ",
"add_member": "Add member",
"remove": "Remove",
"add_user": "Add user",
"users": "Users",
"display_name": "Display name",
"is_admin": "Admin",
"allow_any_scorer_edit": "Allow any scorer to edit penalties",
"open": "Open",
"back": "Back",
"change_password": "Change password",
"new_password": "New password",
"csv_paste": "Paste CSV (number,last,first,country,registration)",
"no_pilots": "No pilots yet",
"no_penalties": "No penalties yet",
"no_members": "No members yet",
"no_competitions": "No competitions",
"select_pilot": "Select pilot",
"rule_number_short": "Rule No.",
"transferred_only": "Only untransferred",
"showing_n_of_m": "Showing {n} of {m}",
"online": "Online",
"offline": "Offline",
"forbidden": "Not allowed",
"save_settings": "Save settings",
"saved": "Saved",
"yes": "Yes",
"no": "No",
"backend_url": "Backend URL",
"backend_url_hint": "Leave empty to use the same origin (e.g. http://192.168.0.10:8080)",
"profile": "Profile",
"current_password": "Current password",
"leave_blank_keep": "Leave blank to keep current",
"username_taken": "Username already taken",
"prior_penalties": "Prior penalties for this pilot and rule",
"none": "None",
"applied": "Applied",
"apply_by_task": "Apply by task",
"apply_by_task_explain": "Confirm all open penalties for a task at once. Penalties are only marked applied after confirmation.",
"apply_n_open": "Apply {n} open",
"confirm_apply_task": "Mark all {n} open penalties for task '{task}' as applied?",
"summary": "Summary",
"penalty_summary": "Penalty summary",
"rule_not_found": "rule not found",
"close": "Close",
"count": "#",
"count_hint": "Number of prior penalties for this pilot and rule",
"prior_count": "Prior count (this pilot & rule)",
"search_penalties": "Search penalties…",
"filter_all": "All",
"filter_open": "Open only",
"filter_applied": "Applied only",
"total": "Total",
"repeat_password": "Repeat password",
"password_too_short": "Password must be at least 8 characters",
"passwords_dont_match": "Passwords do not match",
"too_many_attempts": "Too many login attempts — please wait a few minutes",
"profile_username_readonly": "Username can only be changed by a system administrator",
"profile_displayname_readonly": "Display name can only be changed by a system administrator",
"must_change_password": "Must change password",
"confirm_force_password": "Force this user to change their password on next request?",
"force_password_change": "Force password change",
"force_password_explain": "An administrator has required you to set a new password before you can continue.",
"user_not_found": "User not found",
"show_incidents": "Show incidents",
"hide_incidents": "Hide incidents",
"apply_select_task": "Pick a task to start applying its open penalties.",
"no_open_penalties": "No open penalties to apply.",
"no_task": "(no task)",
"start_apply": "Start",
"step_x_of_y": "Pilot {x} of {y}",
"pilot": "Pilot",
"next": "Next",
"to_overview": "To overview",
"apply_overview": "Overview",
"apply_overview_explain": "{n} penalty/penalties will be marked applied on save.",
"nothing_to_apply": "Nothing to apply.",
"confirm_save_partial": "Save and apply {n} penalty/penalties reviewed so far?",
"closed": "Closed",
"admin_zone": "Administration",
"close_competition": "Close competition",
"reopen_competition": "Reopen competition",
"delete_competition": "Delete competition",
"close_competition_explain": "Closing a competition locks all penalties and writes a CSV backup with the current rule texts. A system administrator can reopen it later.",
"competition_closed_explain": "This competition is closed. Penalties cannot be changed until it is reopened.",
"confirm_close_competition": "Close this competition? Penalties will be locked and a CSV backup will be written.",
"confirm_reopen_competition": "Reopen this competition? Penalties will become editable again.",
"confirm_delete_competition": "Delete this competition? All pilots, penalties and members will be permanently removed.",
"confirm_delete_competition_named": "Delete competition '{name}'? All pilots, penalties and members will be permanently removed.",
"backup_written": "Backup written: {file}",
"competition_closed": "Competition is closed"
}
+83
View File
@@ -0,0 +1,83 @@
{
"login_title": "Iniciar sesión",
"username": "Usuario",
"password": "Contraseña",
"sign_in": "Entrar",
"invalid_credentials": "Usuario o contraseña incorrectos",
"logout": "Salir",
"settings": "Ajustes",
"language": "Idioma",
"competitions": "Competiciones",
"new_competition": "Nueva competición",
"competition_name": "Nombre",
"create": "Crear",
"cancel": "Cancelar",
"role": "Rol",
"system_admin": "Administrador",
"chief_scorer": "Chief-Scorer",
"scorer": "Scorer",
"pilots": "Pilotos",
"penalties": "Penalizaciones",
"members": "Miembros",
"rules": "Reglas",
"settings_tab": "Ajustes",
"number": "Número",
"last_name": "Apellido",
"first_name": "Nombre",
"country": "País",
"balloon_id": "Registration",
"rules_language": "Idioma de las reglas",
"rules_language_hint": "Idioma de los textos de reglas para esta competición (el idioma de la interfaz se mantiene por usuario)",
"add_pilot": "Añadir piloto",
"import_csv": "Importar CSV",
"export_csv": "Exportar CSV",
"flight": "Vuelo",
"date": "Fecha",
"pilot_number": "N.º piloto",
"pilot_name": "Nombre piloto",
"rule": "Regla",
"task": "Tarea",
"penalty_values": "Penalizaciones",
"description": "Descripción",
"created_by": "Creado por",
"transferred": "Transferido",
"actions": "Acciones",
"add_penalty": "Añadir penalización",
"edit": "Editar",
"delete": "Eliminar",
"save": "Guardar",
"confirm_delete": "¿Eliminar este registro?",
"search_rule": "Buscar regla por número o texto",
"suggested_penalty": "Penalización sugerida",
"escalation": "Comportamiento al repetirse",
"escalation_same": "Sin cambios",
"escalation_doubled": "Se duplica cada vez",
"escalation_escalate": "Escala: ",
"add_member": "Añadir miembro",
"remove": "Quitar",
"add_user": "Crear usuario",
"users": "Usuarios",
"display_name": "Nombre mostrado",
"is_admin": "Admin",
"allow_any_scorer_edit": "Cualquier scorer puede editar penalizaciones",
"open": "Abrir",
"back": "Atrás",
"change_password": "Cambiar contraseña",
"new_password": "Nueva contraseña",
"csv_paste": "Pegar CSV (n.º,apellido,nombre,país,registration)",
"no_pilots": "Sin pilotos",
"no_penalties": "Sin penalizaciones",
"no_members": "Sin miembros",
"no_competitions": "Sin competiciones",
"select_pilot": "Elegir piloto",
"rule_number_short": "N.º regla",
"transferred_only": "Solo no transferidas",
"showing_n_of_m": "{n} de {m}",
"online": "En línea",
"offline": "Sin conexión",
"forbidden": "No permitido",
"save_settings": "Guardar ajustes",
"saved": "Guardado",
"yes": "Sí",
"no": "No"
}
+83
View File
@@ -0,0 +1,83 @@
{
"login_title": "Connexion",
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"sign_in": "Connexion",
"invalid_credentials": "Identifiants invalides",
"logout": "Déconnexion",
"settings": "Paramètres",
"language": "Langue",
"competitions": "Compétitions",
"new_competition": "Nouvelle compétition",
"competition_name": "Nom",
"create": "Créer",
"cancel": "Annuler",
"role": "Rôle",
"system_admin": "Administrateur",
"chief_scorer": "Chief-Scorer",
"scorer": "Scorer",
"pilots": "Pilotes",
"penalties": "Pénalités",
"members": "Membres",
"rules": "Règles",
"settings_tab": "Paramètres",
"number": "Numéro",
"last_name": "Nom",
"first_name": "Prénom",
"country": "Pays",
"balloon_id": "Registration",
"rules_language": "Langue des règles",
"rules_language_hint": "Langue des textes de règles pour cette compétition (la langue de l'interface reste par utilisateur)",
"add_pilot": "Ajouter un pilote",
"import_csv": "Importer CSV",
"export_csv": "Exporter CSV",
"flight": "Vol",
"date": "Date",
"pilot_number": "N° pilote",
"pilot_name": "Nom du pilote",
"rule": "Règle",
"task": "Épreuve",
"penalty_values": "Pénalités",
"description": "Description",
"created_by": "Créé par",
"transferred": "Transféré",
"actions": "Actions",
"add_penalty": "Ajouter pénalité",
"edit": "Modifier",
"delete": "Supprimer",
"save": "Enregistrer",
"confirm_delete": "Supprimer cette entrée ?",
"search_rule": "Rechercher une règle par numéro ou texte",
"suggested_penalty": "Pénalité suggérée",
"escalation": "Comportement en cas de répétition",
"escalation_same": "Reste identique",
"escalation_doubled": "Doublée à chaque fois",
"escalation_escalate": "Escalade : ",
"add_member": "Ajouter membre",
"remove": "Retirer",
"add_user": "Créer un utilisateur",
"users": "Utilisateurs",
"display_name": "Nom affiché",
"is_admin": "Admin",
"allow_any_scorer_edit": "Tous les scorers peuvent modifier les pénalités",
"open": "Ouvrir",
"back": "Retour",
"change_password": "Changer le mot de passe",
"new_password": "Nouveau mot de passe",
"csv_paste": "Coller CSV (n°,nom,prénom,pays,registration)",
"no_pilots": "Aucun pilote",
"no_penalties": "Aucune pénalité",
"no_members": "Aucun membre",
"no_competitions": "Aucune compétition",
"select_pilot": "Choisir un pilote",
"rule_number_short": "N° règle",
"transferred_only": "Non transférés uniquement",
"showing_n_of_m": "{n} sur {m}",
"online": "En ligne",
"offline": "Hors ligne",
"forbidden": "Non autorisé",
"save_settings": "Enregistrer",
"saved": "Enregistré",
"yes": "Oui",
"no": "Non"
}
+83
View File
@@ -0,0 +1,83 @@
{
"login_title": "Zaloguj się",
"username": "Nazwa użytkownika",
"password": "Hasło",
"sign_in": "Zaloguj",
"invalid_credentials": "Nieprawidłowy login lub hasło",
"logout": "Wyloguj",
"settings": "Ustawienia",
"language": "Język",
"competitions": "Zawody",
"new_competition": "Nowe zawody",
"competition_name": "Nazwa zawodów",
"create": "Utwórz",
"cancel": "Anuluj",
"role": "Rola",
"system_admin": "Administrator",
"chief_scorer": "Chief-Scorer",
"scorer": "Scorer",
"pilots": "Piloci",
"penalties": "Kary",
"members": "Członkowie",
"rules": "Zasady",
"settings_tab": "Ustawienia",
"number": "Numer",
"last_name": "Nazwisko",
"first_name": "Imię",
"country": "Kraj",
"balloon_id": "Registration",
"rules_language": "Język zasad",
"rules_language_hint": "Język tekstu zasad dla tych zawodów (język interfejsu pozostaje per użytkownik)",
"add_pilot": "Dodaj pilota",
"import_csv": "Import CSV",
"export_csv": "Eksport CSV",
"flight": "Lot",
"date": "Data",
"pilot_number": "Nr pilota",
"pilot_name": "Imię i nazwisko",
"rule": "Zasada",
"task": "Zadanie",
"penalty_values": "Kary",
"description": "Opis",
"created_by": "Wprowadził",
"transferred": "Przesłano",
"actions": "Akcje",
"add_penalty": "Dodaj karę",
"edit": "Edytuj",
"delete": "Usuń",
"save": "Zapisz",
"confirm_delete": "Usunąć ten wpis?",
"search_rule": "Szukaj zasady po numerze lub tekście",
"suggested_penalty": "Sugerowana kara",
"escalation": "Zachowanie przy powtórzeniu",
"escalation_same": "Bez zmian",
"escalation_doubled": "Podwajana za każdym razem",
"escalation_escalate": "Eskalacja: ",
"add_member": "Dodaj członka",
"remove": "Usuń",
"add_user": "Dodaj użytkownika",
"users": "Użytkownicy",
"display_name": "Wyświetlana nazwa",
"is_admin": "Admin",
"allow_any_scorer_edit": "Pozwól dowolnemu scorerowi edytować kary",
"open": "Otwórz",
"back": "Wstecz",
"change_password": "Zmień hasło",
"new_password": "Nowe hasło",
"csv_paste": "Wklej CSV (nr,nazwisko,imię,kraj,registration)",
"no_pilots": "Brak pilotów",
"no_penalties": "Brak kar",
"no_members": "Brak członków",
"no_competitions": "Brak zawodów",
"select_pilot": "Wybierz pilota",
"rule_number_short": "Nr zasady",
"transferred_only": "Tylko nieprzesłane",
"showing_n_of_m": "{n} z {m}",
"online": "Online",
"offline": "Offline",
"forbidden": "Brak uprawnień",
"save_settings": "Zapisz ustawienia",
"saved": "Zapisano",
"yes": "Tak",
"no": "Nie"
}
+83
View File
@@ -0,0 +1,83 @@
{
"login_title": "Entrar",
"username": "Utilizador",
"password": "Palavra-passe",
"sign_in": "Entrar",
"invalid_credentials": "Utilizador ou palavra-passe inválidos",
"logout": "Sair",
"settings": "Definições",
"language": "Idioma",
"competitions": "Competições",
"new_competition": "Nova competição",
"competition_name": "Nome",
"create": "Criar",
"cancel": "Cancelar",
"role": "Papel",
"system_admin": "Administrador",
"chief_scorer": "Chief-Scorer",
"scorer": "Scorer",
"pilots": "Pilotos",
"penalties": "Penalizações",
"members": "Membros",
"rules": "Regras",
"settings_tab": "Definições",
"number": "Número",
"last_name": "Apelido",
"first_name": "Nome",
"country": "País",
"balloon_id": "Registration",
"rules_language": "Idioma das regras",
"rules_language_hint": "Idioma dos textos das regras desta competição (o idioma da interface mantém-se por utilizador)",
"add_pilot": "Adicionar piloto",
"import_csv": "Importar CSV",
"export_csv": "Exportar CSV",
"flight": "Voo",
"date": "Data",
"pilot_number": "N.º piloto",
"pilot_name": "Nome do piloto",
"rule": "Regra",
"task": "Tarefa",
"penalty_values": "Penalizações",
"description": "Descrição",
"created_by": "Criado por",
"transferred": "Transferido",
"actions": "Ações",
"add_penalty": "Adicionar penalização",
"edit": "Editar",
"delete": "Eliminar",
"save": "Guardar",
"confirm_delete": "Eliminar este registo?",
"search_rule": "Procurar regra por número ou texto",
"suggested_penalty": "Penalização sugerida",
"escalation": "Comportamento em caso de repetição",
"escalation_same": "Sem alteração",
"escalation_doubled": "Duplicada de cada vez",
"escalation_escalate": "Escala: ",
"add_member": "Adicionar membro",
"remove": "Remover",
"add_user": "Criar utilizador",
"users": "Utilizadores",
"display_name": "Nome mostrado",
"is_admin": "Admin",
"allow_any_scorer_edit": "Qualquer scorer pode editar penalizações",
"open": "Abrir",
"back": "Voltar",
"change_password": "Alterar palavra-passe",
"new_password": "Nova palavra-passe",
"csv_paste": "Colar CSV (n.º,apelido,nome,país,registration)",
"no_pilots": "Sem pilotos",
"no_penalties": "Sem penalizações",
"no_members": "Sem membros",
"no_competitions": "Sem competições",
"select_pilot": "Escolher piloto",
"rule_number_short": "N.º regra",
"transferred_only": "Apenas não transferidas",
"showing_n_of_m": "{n} de {m}",
"online": "Online",
"offline": "Offline",
"forbidden": "Não autorizado",
"save_settings": "Guardar definições",
"saved": "Guardado",
"yes": "Sim",
"no": "Não"
}
+83
View File
@@ -0,0 +1,83 @@
{
"login_title": "Вход",
"username": "Имя пользователя",
"password": "Пароль",
"sign_in": "Войти",
"invalid_credentials": "Неверное имя или пароль",
"logout": "Выйти",
"settings": "Настройки",
"language": "Язык",
"competitions": "Соревнования",
"new_competition": "Новое соревнование",
"competition_name": "Название",
"create": "Создать",
"cancel": "Отмена",
"role": "Роль",
"system_admin": "Администратор",
"chief_scorer": "Chief-Scorer",
"scorer": "Scorer",
"pilots": "Пилоты",
"penalties": "Штрафы",
"members": "Участники",
"rules": "Правила",
"settings_tab": "Настройки",
"number": "Номер",
"last_name": "Фамилия",
"first_name": "Имя",
"country": "Страна",
"balloon_id": "Registration",
"rules_language": "Язык правил",
"rules_language_hint": "Язык текстов правил для этого соревнования (язык интерфейса — индивидуальный)",
"add_pilot": "Добавить пилота",
"import_csv": "Импорт CSV",
"export_csv": "Экспорт CSV",
"flight": "Полёт",
"date": "Дата",
"pilot_number": "№ пилота",
"pilot_name": "Имя пилота",
"rule": "Правило",
"task": "Задание",
"penalty_values": "Штрафы",
"description": "Описание",
"created_by": "Автор",
"transferred": "Передано",
"actions": "Действия",
"add_penalty": "Добавить штраф",
"edit": "Редактировать",
"delete": "Удалить",
"save": "Сохранить",
"confirm_delete": "Удалить запись?",
"search_rule": "Поиск правила по номеру или тексту",
"suggested_penalty": "Рекомендованный штраф",
"escalation": "Поведение при повторе",
"escalation_same": "Без изменений",
"escalation_doubled": "Удваивается каждый раз",
"escalation_escalate": "Эскалация: ",
"add_member": "Добавить участника",
"remove": "Удалить",
"add_user": "Создать пользователя",
"users": "Пользователи",
"display_name": "Отображаемое имя",
"is_admin": "Админ",
"allow_any_scorer_edit": "Любой Scorer может редактировать штрафы",
"open": "Открыть",
"back": "Назад",
"change_password": "Изменить пароль",
"new_password": "Новый пароль",
"csv_paste": "Вставьте CSV (№,фамилия,имя,страна,registration)",
"no_pilots": "Нет пилотов",
"no_penalties": "Нет штрафов",
"no_members": "Нет участников",
"no_competitions": "Нет соревнований",
"select_pilot": "Выберите пилота",
"rule_number_short": "№ правила",
"transferred_only": "Только непереданные",
"showing_n_of_m": "{n} из {m}",
"online": "Онлайн",
"offline": "Оффлайн",
"forbidden": "Нет доступа",
"save_settings": "Сохранить настройки",
"saved": "Сохранено",
"yes": "Да",
"no": "Нет"
}
+3 -1
View File
@@ -1,6 +1,8 @@
(async function () {
const root = document.getElementById("app");
await setLang(detectInitialLang());
// If already signed in, skip the login form.
try {
const u = await API.me();
@@ -17,7 +19,7 @@
const passwordInput = el("input", { type: "password", autocomplete: "current-password", placeholder: t("password") });
const errorBox = el("div", { class: "muted", style: { color: "var(--danger)", display: "none" } });
const langSelect = el("select",
{ onchange: (e) => { setLang(e.target.value); render(); } },
{ onchange: async (e) => { await setLang(e.target.value); render(); } },
...I18N_AVAILABLE.map((l) => el("option", { value: l, selected: l === CURRENT_LANG }, I18N_NAMES[l]))
);