Files
OrcaSlicer/src/slic3r/GUI/IMEXModesCtrl.hpp
T
Ian Bassi e821665e4b Add orca-translations Claude skill and PO tool (#16436)
* Add orca-translations Claude skill and PO tool

Add a .claude skill that guides translation passes over the localization/i18n gettext catalogs. It covers the workflow: run gettext, settle new glossary terms, translate in batches, verify, and report.

Includes translation-rules.md, which condenses the placeholder, line-break and glossary rules plus the guidance on review notes.

Also adds scripts/po_tool.py (standard library only). It lists pending entries with glossary hints, validates and applies translations, adding '# AI Translated' and '# Needs human review' comments, and rewrites only the entries it changes. It can also check catalogs, look up existing translations, suggest glossary candidates and update the wiki glossary TSV.

* Let po_tool apply translations by entry id

pending now gives each entry a short id, so a translation file can name
entries by id instead of repeating the English msgid. apply accepts either.

A "review": "" item clears an earlier review note when overwriting an
entry, and check gives each msgfmt run its own temporary output file so
parallel checks do not collide.

* Fill all empty translations

Regenerate the template with run_gettext --full and translate every empty
entry in the 22 catalogs (7,089 entries) with the orca-translations skill,
following the localization glossary, including the new belt printer,
IDEX/IQEX and plugin terms. Every new entry is marked "# AI Translated";
122 also carry "# Needs human review" with what to check.

* Show entry ids in po_tool lookup and search translations

lookup now prints each entry's id, so an entry found there can be
rewritten with apply --overwrite without repeating the English, and
--msgstr searches the translations instead of the msgids, which finds
every entry that still uses a term being changed.

SKILL.md describes how to change an established term: check the
catalog's usage first, update the glossary cell, then rewrite only the
entries where the term has that meaning.

* Fix mistranslated terms and align catalogs with the glossary

Correct labels that the glossary had copied, and the entries that used
them:
- ca: Raft was rendered as the Brim term; it is now "Rai". Exhaust fan,
  Monotonic line, Unretract, Slice, Lateral Honeycomb and PA Tower
  labels fixed.
- cs: Lightning infill is "Blesk", not "Osvětlení" (illumination).
- sv: Retraction is "Retraktion", not "Reduktion"; Unretract,
  Lagerhöjd, Delkylningsfläkt, Utsugsfläkt, Aligned Rectilinear and the
  Preset label fixed.
- de: Brim is "Rand" and Skirt is "Umrandung", as most entries already
  had them.
- lt: brim entries used the Skirt word "apvadas"; they now use "kraštas".
- es: Sparse infill is "relleno de baja densidad", the catalog majority.
- eu: nozzle wipe is "garbiketa", not purge.
- th: the project plate is "เพลต", distinct from the bed.
- uk: Quarter Cubic, Latin "PA" in PA Tower, and Belt purge tower.
- pl: Cross Hatch, Tri-hexagon, Volumetric speed and Raft labels.
- zh_TW: Brim stays "Brim", as the existing labels have it.
- tr: "Prime Tower" read "Prime Minister Tower".
- ko: the plugin "Capabilities" label read "performance".
- Build Volume labels fixed in tr, de, cs and eu.

All 242 rewritten entries are marked "# AI Translated".

* Make more UI text translatable and fix pluralization and context

Audit the sources for text that translators could not translate
correctly and fix it.

Mark text that reached users in English only: IDEX/IQEX role names,
plugin permission prompts and errors, 3MF/preset loading errors, G-code
placeholder parser errors, calibration and device messages, notification
texts, file dialog filters, and the config validation messages.

Replace sentences built from translated fragments, such as the prime
tower warnings, "left"/"right" nozzle names, and "Filament " + number,
with single msgids that have placeholders, so translators can reorder
and inflect them.

Use plural forms for texts with a count ("%1% mixed filaments",
"%d objects", "%1% triangles") instead of "(s)" and mismatched verbs.
Fix the swapped singular/plural pair in "The following presets inherit
this preset".

Add translation contexts where one English word has several meanings
(Subscribed/Local, Standard, Pause, View, Step, Model, Camera, Flow,
Filter, Others, Prepare, the Design tools). Show "Size" instead of
"Build Volume" in the Cut tool, which displays the object size.

Fix code that compared translated text with English (the Edit G-code
dialog group, the plugin registration failure check), and add the
bundled web pages' missing string-table entries. Use the single
character for degrees Celsius, say "G-code", and describe the head
filament map without internal jargon.

Install the libslic3r translate callback before the presets load, so
their load errors are translated, and list the 28 source files that had
marked strings but were never extracted into localization/i18n/list.txt.

* Suggest earlier translations and list fuzzy entries in po_tool pending

pending now attaches the closest earlier translation of a similar
English text (from obsolete or translated entries), so a reworded or
split string keeps its wording, and it lists fuzzy entries by default,
since users never see them. A fuzzy entry also carries its stale
translation as previous_translation.

* Fix wording of the non-boolean operands error

The logical operation error said "operators" where it means the
operands the operation was applied to.

* Translate the newly translatable strings in all languages

Regenerate the template after the translatability changes and fill every
new or changed entry in the 22 catalogs (about 12,800 entries): the
strings that became translatable, the plural forms, the entries split by
context, and the previously fuzzy singular entries that became plurals.

Every new entry is marked "# AI Translated", and about 360 also carry a
"# Needs human review" note saying what to check. Terms follow the
localization glossary. Earlier translations of reworded strings were
reused where the meaning was unchanged. Existing entries were not
changed, and obsolete entries are kept.

* Document the source translatability audit in the orca-translations skill

Add a reference describing how strings reach translators, what to look
for (untranslated text, spliced sentences, missing plurals, ambiguous
words), what not to translate, where a context cannot be carried, and
what to do after the edits, plus a scanner that lists untranslated
user-facing string literals.

* Document how to write translatable strings in AGENTS.md

Tell developers to keep each message complete in one msgid: use
placeholders instead of concatenating translated fragments, plural forms
for counts, and contexts for ambiguous words, and to list new source
files in localization/i18n/list.txt. Add a matching code review item.

* Translate the strings added since the rebase

Regenerate the template against the current main and translate the 26
new entries per language: the Fix Model commands and messages, and the
texture displacement image adjustment panel. Each new entry is marked
"# AI Translated"; existing entries are unchanged.

* tidy
2026-10-13 10:53:57 -03:00

270 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#ifndef slic3r_IMEXModesCtrl_hpp_
#define slic3r_IMEXModesCtrl_hpp_
#include <libslic3r/PrintConfig.hpp>
#include <utility>
#include <wx/colour.h>
#include <wx/panel.h>
#include <wx/string.h>
#include <wx/sizer.h>
#include <wx/button.h>
#include <wx/textctrl.h>
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <tuple>
#include <vector>
#include "libslic3r/IMEXHelpers.hpp"
class ScalableButton;
class ComboBox;
namespace Slic3r {
namespace GUI {
// ---------------------------------------------------------------------------
// IMEXModesCtrl — visual parallel-mode editor for the IDEX/IQEX printer tab
//
// A tool tile holds an `std::optional<ImexRole>`: the role the mode gives that tool, or
// nullopt for Inactive (gray — the tool does not take part in this mode). Inactive is the
// editor's own state and the only reason the tile is not a bare ImexRole; it is the absence
// of a role, so it is spelled as one rather than as an extra enumerator that libslic3r would
// then have to keep rejecting. Roles themselves are never re-encoded here — no int state
// table, no second copy of the on-disk letters.
//
// nullopt (gray) — not participating in this mode
// ImexRole::Primary (green) — the tool the slicer generates paths for
// ImexRole::Copy (blue) — firmware duplicates Primary at an offset
// ImexRole::Mirror (amber) — firmware mirrors Primary about an axis
// ImexRole::Span (yellow)— multicolor partner of Primary on the same gantry;
// declares paired-gantry multicolor topology and
// unlocks the multicolor block at slice time. Only
// offered on tools sharing primary's gantry, and
// only on multi-gantry printers (n_rows >= 2).
//
// Color and legend text are the editor's business and live in role_style(); the tile cycle
// order and the serialized letters come from kImexRoleTable in IMEXHelpers.hpp, so the
// editor cannot drift from the slicer's reading of a mode string.
//
// Active-tools string format: "idx:P,idx:C,idx:M,idx:S" (backwards-compat: plain "idx" = Primary)
// ---------------------------------------------------------------------------
class IMEXModesCtrl : public wxPanel {
public:
std::function<void()> on_change;
// Fired after on_change has written the shortened table back to the preset, with EVERY
// name the deleted row could be known by: the name the row was built with (what a plate
// set from the saved preset holds) and the name in the field at delete time (what a plate
// set after an in-session rename holds). Renaming is deliberately not routed through here
// -- the name field notifies on every keystroke -- so by the time the row is deleted the
// two can differ, and reporting only the current name leaves the plate pointing at a mode
// that no longer exists. Names a surviving row still carries are filtered out, so renaming
// one row to another's old name and then deleting cannot reset a plate that legitimately
// resolves elsewhere. Plates still set to any reported name are stale from this point on
// -- see the handler in TabPrinter::build_fff.
std::function<void(const std::vector<std::string>&)> on_mode_removed;
// Lazy lookup so the widget always sees the current parent (preset switches
// change the parent under us). Returns nullptr when the active preset has no
// parent (system preset itself), which disables per-row reset arrows.
using ParentConfigLookup = std::function<const DynamicPrintConfig*()>;
void set_parent_config_lookup(ParentConfigLookup f) { m_parent_lookup = std::move(f); }
// layout: 0=front-left, 1=front-right, 2=rear-left, 3=rear-right (T0 corner).
// The enum values intentionally line up 1:1 so this is a no-op cast — the helper
// is kept to make the conversion explicit at call sites.
static int parse_layout(ImexToolLayout layout) { return static_cast<int>(layout); }
IMEXModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layout = 0);
void set_grid_size(int n_cols, int n_rows, int layout = -1);
void load_from_config(const DynamicPrintConfig& cfg);
// Every row on screen produces exactly one entry — a row is never skipped.
// Skipping empty-named rows used to drop the row's tool roles and G-code
// silently, and because matches_config() compares against this same filtered
// output the preset did not even go dirty to hint that something had gone
// missing; the row stayed on screen until the next load_from_config() quietly
// removed it. add_row() pre-fills a name and the Name field restores one when
// it is left empty, so the substitution below is the last-resort guard for a
// name that arrives empty from a hand-edited preset or 3MF.
std::tuple<std::vector<std::string>, std::vector<std::string>, std::vector<std::string>>
get_mode_data() const;
// True when the widget's current rows already mirror the three IMEX-mode option
// vectors in cfg. Used by TabPrinter::update_fff() to skip a destroy-and-rebuild
// pass when the only thing that just changed was the widget itself writing back
// to config — without this guard, every keystroke in the gcode textbox triggers
// a load_from_config() that detaches the textbox the user is typing in.
bool matches_config(const DynamicPrintConfig& cfg) const;
// Whether the printer is an IMEX printer. The grid sits in an options group that shows or hides
// everything in it by mode, so Show() keeps it hidden on other printers whatever the group asks.
void set_applicable(bool applicable)
{
m_applicable = applicable;
if (!applicable)
wxPanel::Show(false); // showing is left to the group, which knows the mode
}
bool Show(bool show = true) override { return wxPanel::Show(show && m_applicable); }
private:
bool m_applicable = true;
// map<physical tool idx, role> for one `imex_mode_active_tools` entry. A tool absent
// from the map is Inactive — the same convention the serialized string uses, so the
// round trip needs no separate "off" value.
//
// Deliberately NOT a parser of its own: the slicer decides what actually prints,
// so the editor has to read the string exactly the way the slicer does or the
// same mode means two different things in the two places. Roles come from
// parse_imex_active_tools(); the primary index comes from
// imex_primary_tool_for_mode(), which is where the "a bare index is Primary"
// backwards-compat rule lives. Both are needed: parse_imex_active_tools()
// defaults a bare token to Copy (deliberately — see the note above
// parse_imex_active_tools() in IMEXHelpers.hpp), so only the combination
// reproduces the slicer's reading of a legacy "0,1,2" string, which is
// "T0 primary, the rest copies".
//
// Indices are PHYSICAL tool indices (T0..TN-1). all_tool_roles preserves
// off-grid entries so the tile widget can round-trip indices that fall
// outside the currently visible rows × cols without losing data on save.
static std::map<int, ImexRole> roles_for_mode(const std::string& active_tools);
// `base` + " N" for the lowest N >= 2 not already used by a row or by `also_taken`
// (N == 1 is the fixed Primary row, or the name being made unique). Deterministic for a
// given set of rows, so get_mode_data() is stable across calls and matches_config() stays
// honest. Intentionally NOT translated: mode names are identifiers — objects
// store one in `imex_parallel_mode` and GCode.cpp matches it against
// `imex_mode_names` by string — so a locale-dependent name would break a project
// opened under a different language.
std::string unique_mode_name(const std::vector<std::string>& also_taken, const std::string& base = "Mode") const;
// Everything the editor draws for one role, in ONE place: a new role needs a color and
// a legend name here and nowhere else in this file. nullopt is Inactive (gray).
// `label` is display-only and translated; the preset stores the role letters, never it.
struct RoleStyle { wxColour bg; wxColour fg; wxString label; };
static RoleStyle role_style(std::optional<ImexRole> role);
// True when this printer offers `role` at all, regardless of which tile is clicked.
// Span needs a second gantry to be a partner across, so a single-gantry printer neither
// cycles to it nor lists it in the legend.
bool role_offered(ImexRole role) const;
// True when the tile for `tool_idx` may take `role` right now. Adds the positional rules
// on top of role_offered(): only one tile may hold Primary, and Span only on a tile
// sharing the Primary's gantry.
bool role_allowed_on_tile(ImexRole role, int tool_idx,
bool other_primary, int primary_gantry) const;
// Next state in the tile cycle: Inactive → kImexRoleTable order → Inactive, stepping
// over any role `allowed` rejects. Inactive is always reachable, so it terminates.
static std::optional<ImexRole> next_tile_role(std::optional<ImexRole> current,
const std::function<bool(ImexRole)>& allowed);
// Mode names this printer's tool grid can actually carry, minus those already taken.
std::vector<wxString> suggested_mode_names(const ::ComboBox* skip = nullptr) const;
void apply_btn(wxButton* btn, int tool_idx, std::optional<ImexRole> role);
struct Row {
wxPanel* panel;
::ComboBox* name; // nullptr for primary row (name is fixed)
wxTextCtrl* gcode;
std::vector<wxButton*> btns;
std::vector<std::optional<ImexRole>> btn_roles; // nullopt == Inactive
std::vector<int> btn_tool_idx;
std::map<int, ImexRole> all_tool_roles; // absent == Inactive
// Name the row was built with (UTF-8), i.e. the name that was in the preset — and so
// in the plate's mode selector — when the row appeared. Kept because the name field is
// free to drift from it without the row being rebuilt; remove_row() needs both to find
// the plates a delete strands. Rebuilding the rows (load_from_config / set_grid_size)
// re-seeds it, which is correct: config and rows agree again at that point.
std::string orig_name;
// The name when the field last took focus, so a commit that leaves it unchanged is not
// treated as an edit.
std::string focus_name;
bool is_primary {false};
ScalableButton* reset_btn {nullptr}; // nullptr when row has no parent counterpart
bool reset_dirty_cached {false};
};
// Snapshot of row content as it would be saved (matches get_mode_data() per-row).
struct RowSnapshot { std::string name, tools, gcode; };
RowSnapshot snapshot_row(const Row& r) const;
// True when row at `row_idx` differs from the parent preset's value at the same
// index. Returns false when there is no parent (system preset), when the row is
// beyond the parent's mode count (user added it — no defined "default"), or when
// the lookup callback isn't wired. The Primary row's name is sentinel-fixed so
// a name diff doesn't count for it; only tools/gcode do.
bool row_differs_from_parent(int row_idx) const;
// True when row index has a parent counterpart at all (i.e., the reset arrow
// should be present on the row at all, regardless of dirty state).
bool row_has_parent_counterpart(int row_idx) const;
void add_row(const std::string& name = "",
const std::string& active_tools = "",
const std::string& gcode = "",
bool is_primary = false);
// Returns every name the removed row was known by (UTF-8, deduplicated: its build-time
// name and its current name, which differ after an in-session rename), minus any name a
// surviving row still carries. The caller uses them to find the plates the delete
// stranded. Empty when nothing was removed, when the row is the non-deletable Primary,
// or when both names are blank -- a blank name is only reachable on a row that was never
// named, which no plate can be holding.
std::vector<std::string> remove_row(wxPanel* panel);
void clear_rows();
std::string active_tools_string(const Row& r) const;
// Replaces an edited row's name when it is empty, the reserved Primary name in any case, or
// one another row already carries: a plate stores the name and find_imex_mode() takes its
// first row, so the edited row yields. Returns whether it changed the name; the caller notifies.
bool fix_row_name(Row& r);
void notify();
// Update each row's reset bitmap to reflect current dirty state. Cached so we
// only swap the bitmap when state actually transitions — avoids flicker on
// every keystroke. Disabled buttons (no saved counterpart) keep their dot.
void refresh_reset_buttons();
// Reset row identified by `panel` (stable across add/remove) to the parent
// preset's value at the same index. Primary row's name stays sentinel-fixed —
// only tools and gcode are restored.
void reset_row_to_parent(wxPanel* panel);
// Rebuild the instruction text, color legend and column headers for the CURRENT grid.
// These depend on the grid shape -- the Span role is only offered on a multi-gantry printer,
// and the header spacing is derived from the column count -- so they cannot be built once in
// the constructor: adding a second gantry has to reveal Span without a save-and-reopen cycle.
void rebuild_info_and_header();
// One-line explanation per role, shown as a tooltip on that role's legend swatch and label.
// Keeps the per-role detail out of the instruction paragraph.
static wxString role_hint(ImexRole role);
wxBoxSizer* m_outer;
wxBoxSizer* m_rows_sizer;
wxPanel* m_info_panel = nullptr;
wxPanel* m_hdr_panel = nullptr;
std::vector<Row> m_rows;
int m_n_cols, m_n_rows;
int m_layout {0}; // 0=front-left, 1=front-right, 2=rear-left, 3=rear-right
bool m_clearing_rows {false}; // guards handlers against teardown-time events
ParentConfigLookup m_parent_lookup;
};
}} // namespace Slic3r::GUI
#endif // slic3r_IMEXModesCtrl_hpp_