foxygit / RPA-Remisser Log in
commit 49aebf0f153cbc4a4cef56b6122a8ab8a36da9d5
Author:     Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
AuthorDate: Fri Aug 28 13:27:21 2026 +0200
Commit:     Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
CommitDate: Fri Aug 28 13:27:21 2026 +0200

    Add control flow, debug tooling, and full i18n to the flow builder

    - if/else-if/else blocks and labeled jump_to_label with infinite-jump guard
    - variables debug panel (click a var to inspect its value/type)
    - list step now includes a text preview of the first matches
    - English-default UI with a Swedish translation, switchable via a language
      selector; drop BankID-specific wording (tool supports any manual login)
    - README updated to document the new steps
---
 README.md                  |  20 ++
 rpa/flow_engine.py         | 129 +++++++-
 rpa/runs.py                |   5 +-
 webui/templates/index.html | 764 +++++++++++++++++++++++++++++++++++++++------
 4 files changed, 806 insertions(+), 112 deletions(-)

diff --git a/README.md b/README.md
index 6d0e846..f065188 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,18 @@ I UI:t bygger du ett flöde som en lista av steg:
   felsökning av flöden)
 - **Lista poster** – hittar alla element som matchar en CSS-selector och
   sparar dem under ett variabelnamn (t.ex. `poster`)
+- **Om / annars om / annars** ("if") – kör bara sina inre steg om ett
+  villkor stämmer. Första grenen ("Om") vars villkor är sant körs; fler
+  grenar kan läggas till som "...annars om"; en valfri "...annars
+  (else)"-gren körs om ingen av grenarna matchade. Villkor: "Element
+  finns"/"Element finns inte" (samma via/text/CSS/roll som andra steg)
+  eller "Variabel är lika med"/"är INTE lika med"/"innehåller" (jämför
+  mot en variabels textinnehåll).
+- **Hoppa till etikett** ("goto") – hoppar till ett annat steg som fått
+  samma text i sitt "Etikett"-fält (syns bredvid typ-väljaren på varje
+  steg), framåt eller bakåt. Fungerar bara mellan steg på samma nivå -
+  kan inte hoppa in i eller ut ur en Loop/If-gren. Ett skydd stoppar
+  körningen om för många hopp sker (misstänkt oändlig loop).
 - **Loop (för varje post)** – itererar över en variabel från "Lista poster";
   steg inuti loopen kan sätta Sökområde = "item" för att bara söka inom
   aktuell post
@@ -118,6 +130,14 @@ Bredvid "Kör flöde" väljer du vad som ska hända med webbläsaren efteråt:
 När webbläsaren hålls öppen visas en "Stäng webbläsare"-knapp i sidan –
 klicka på den när du är klar med att inspektera.

+### Se variablernas innehåll
+
+Under körloggen finns en "Variabler"-panel som fylls i när flödet är klart
+(eller vid fel). Klicka på ett variabelnamn för att expandera och se dess
+faktiska innehåll (text visas rakt av, listor/objekt som JSON) – praktiskt
+för att felsöka t.ex. vad "Lägg till i lista" faktiskt samlade ihop innan
+det sparades till CSV.
+
 ### Felsöka en körning (trace)

 Varje körning spelar automatiskt in en Playwright-trace – skärmdumpar,
diff --git a/rpa/flow_engine.py b/rpa/flow_engine.py
index 3e17dcb..c42dc59 100644
--- a/rpa/flow_engine.py
+++ b/rpa/flow_engine.py
@@ -26,11 +26,32 @@ Stegtyper:
                                                                     -- stdout (JSON om möjligt) i var
   screenshot        {name?}                                     -- sparar skärmdump i data/screenshots
   list              {value, var}                       -- CSS-selector, sparar antal+selector i var
+                                                          (samt en textförhandsvisning av de första 3
+                                                          träffarna, till hjälp vid felsökning i UI:ts
+                                                          variabelpanel - loop-steget bryr sig bara om
+                                                          selector/count)
   loop              {over, steps: [...]}                -- itererar över "list"-resultat
   download          {by, value, role?, scope?, nth?}
+  if                {branches: [{condition, steps: [...]}, ...], else?: [...]}
+                                                          -- första grenen vars condition är sann körs
+                                                          -- (branches[0] = "if", övriga = "else if");
+                                                          -- körs ingen körs "else" (om satt)
+  jump_to_label     {target}                              -- hoppar till steget vars "label" matchar "target", i
+                                                            -- SAMMA stegdel (bara syskon, inte in i/ut ur
+                                                            -- en loop/if); FlowError om etiketten saknas
+                                                            -- eller om för många hopp sker (skydd mot
+                                                            -- oändlig loop)
+
+condition (för "if"-grenar): {kind, ...}
+  kind: "element_exists" | "element_not_exists"  -- {by, value, role?, scope?, nth?}
+      | "var_equals" | "var_not_equals" | "var_contains"  -- {var, text}

 by: "text" | "css" | "role"
 scope: "page" (default) | "item"  -- "item" begränsar sökningen till aktuell loop-post
+label: valfri nyckel på vilket steg som helst - namnger steget som mål för "jump_to_label"
+enabled: valfri nyckel på vilket steg som helst - satt till false hoppar
+         körningen över steget (om det är en "loop"/"if" skippas hela
+         blocket inklusive dess inre steg).
 """

 import csv
@@ -83,6 +104,32 @@ def _selector_or_literal_text(page: Page, item_locator, step: dict) -> str:
     return step.get("text", "")


+def _evaluate_condition(page: Page, item_locator, vars_: dict, condition: dict) -> bool:
+    kind = condition.get("kind", "element_exists")
+
+    if kind in ("element_exists", "element_not_exists"):
+        loc = _resolve_locator(page, item_locator, condition)
+        exists = loc.count() > 0
+        return exists if kind == "element_exists" else not exists
+
+    if kind in ("var_equals", "var_not_equals", "var_contains"):
+        actual = vars_.get(condition.get("var", ""), "")
+        actual_str = actual if isinstance(actual, str) else json.dumps(actual, ensure_ascii=False)
+        expected = condition.get("text", "")
+        if kind == "var_equals":
+            return actual_str == expected
+        if kind == "var_not_equals":
+            return actual_str != expected
+        return expected in actual_str
+
+    raise FlowError(f"Okänt villkor: {kind!r}")
+
+
+# Skydd mot oändliga hopp via jump_to_label - delas (muteras) genom hela
+# körningens rekursiva _run_steps-anrop.
+MAX_JUMPS_PER_RUN = 1000
+
+
 def _run_steps(
     page: Page,
     steps: list,
@@ -90,10 +137,36 @@ def _run_steps(
     vars_: dict,
     item_locator,
     download_dir: Path,
+    jump_budget: dict,
 ) -> None:
-    for step in steps:
+    i = 0
+    while i < len(steps):
+        step = steps[i]
         t = step.get("type")

+        if step.get("enabled") is False:
+            log(f"(Inaktiverat, hoppar över: {t})")
+            i += 1
+            continue
+
+        if t == "jump_to_label":
+            target_label = step.get("target", "")
+            target_index = next((j for j, s in enumerate(steps) if s.get("label") == target_label), None)
+            if target_index is None:
+                raise FlowError(
+                    f"Hittar ingen etikett '{target_label}' bland stegen på samma nivå "
+                    "(kan inte hoppa in i/ut ur en loop eller if-gren)."
+                )
+            jump_budget["remaining"] -= 1
+            if jump_budget["remaining"] <= 0:
+                raise FlowError(
+                    "För många hopp i den här körningen - avbryter "
+                    "(troligen en oändlig hopp-loop)."
+                )
+            log(f"Hoppar till etikett '{target_label}'")
+            i = target_index
+            continue
+
         if t == "goto":
             log(f"Går till {step['url']}")
             page.goto(step["url"], wait_until="domcontentloaded")
@@ -274,8 +347,15 @@ def _run_steps(
         elif t == "list":
             selector = step["value"]
             var_name = step["var"]
-            count = page.locator(selector).count()
-            vars_[var_name] = {"selector": selector, "count": count}
+            loc = page.locator(selector)
+            count = loc.count()
+            preview = []
+            for preview_idx in range(min(count, 3)):
+                try:
+                    preview.append(loc.nth(preview_idx).inner_text(timeout=2000)[:200])
+                except Exception:  # noqa: BLE001
+                    preview.append("<kunde inte läsa text>")
+            vars_[var_name] = {"selector": selector, "count": count, "preview": preview}
             log(f"Hittade {count} post(er) -> variabel '{var_name}'")

         elif t == "loop":
@@ -287,10 +367,25 @@ def _run_steps(
                 )
             selector = over["selector"]
             count = over["count"]
-            for i in range(count):
-                log(f"-- Post {i + 1}/{count} --")
-                current_item = page.locator(selector).nth(i)
-                _run_steps(page, step.get("steps", []), log, vars_, current_item, download_dir)
+            for post_idx in range(count):
+                log(f"-- Post {post_idx + 1}/{count} --")
+                current_item = page.locator(selector).nth(post_idx)
+                _run_steps(page, step.get("steps", []), log, vars_, current_item, download_dir, jump_budget)
+
+        elif t == "if":
+            matched_steps = None
+            for branch in step.get("branches", []):
+                if _evaluate_condition(page, item_locator, vars_, branch.get("condition", {})):
+                    matched_steps = branch.get("steps", [])
+                    break
+            if matched_steps is not None:
+                log("Villkor uppfyllt, kör gren")
+                _run_steps(page, matched_steps, log, vars_, item_locator, download_dir, jump_budget)
+            elif step.get("else") is not None:
+                log("Inget villkor uppfyllt, kör 'else'-gren")
+                _run_steps(page, step["else"], log, vars_, item_locator, download_dir, jump_budget)
+            else:
+                log("Inget villkor uppfyllt, ingen 'else'-gren - fortsätter")

         elif t == "download":
             loc = _resolve_locator(page, item_locator, step)
@@ -307,6 +402,8 @@ def _run_steps(
         else:
             raise FlowError(f"Okänd stegtyp: {t!r}")

+        i += 1
+

 def run_flow(
     steps: list,
@@ -314,7 +411,7 @@ def run_flow(
     download_dir: Path = None,
     close_mode: str = "always",
     close_event=None,
-    on_finished: Callable[[Exception | None], None] | None = None,
+    on_finished: Callable[[Exception | None, dict], None] | None = None,
     trace_path: Path | None = None,
 ) -> None:
     """Kör ett flöde.
@@ -324,9 +421,10 @@ def run_flow(
       "on_success" -- stäng bara om inget steg gav fel, håll öppen vid fel
       "never"      -- håll alltid öppen tills close_event sätts

-    on_finished(error) anropas direkt när stegen är klara (innan ev. väntan
-    på close_event), så anroparen kan uppdatera körningens status utan att
-    behöva vänta på att webbläsaren faktiskt stängs.
+    on_finished(error, vars_) anropas direkt när stegen är klara (innan ev.
+    väntan på close_event), så anroparen kan uppdatera körningens status och
+    visa variablernas innehåll utan att behöva vänta på att webbläsaren
+    faktiskt stängs.

     Om trace_path anges spelas en Playwright-trace (skärmdumpar, DOM-snapshots,
     nätverk) in under hela körningen och sparas dit. Öppna den efteråt med
@@ -340,15 +438,20 @@ def run_flow(
             context.tracing.start(screenshots=True, snapshots=True, sources=True)
         page = get_page(context)
         error: Exception | None = None
+        vars_: dict = {}
+        jump_budget = {"remaining": MAX_JUMPS_PER_RUN}
         try:
-            _run_steps(page, steps, log, vars_={}, item_locator=None, download_dir=download_dir)
+            _run_steps(
+                page, steps, log, vars_=vars_, item_locator=None,
+                download_dir=download_dir, jump_budget=jump_budget,
+            )
             log("Flöde klart.")
         except Exception as exc:  # noqa: BLE001
             error = exc
             log(f"FEL: {exc}")

         if on_finished is not None:
-            on_finished(error)
+            on_finished(error, vars_)

         should_close_now = close_mode == "always" or (close_mode == "on_success" and error is None)
         if not should_close_now:
diff --git a/rpa/runs.py b/rpa/runs.py
index a4bad18..bd2088e 100644
--- a/rpa/runs.py
+++ b/rpa/runs.py
@@ -29,11 +29,13 @@ def start_run(steps: list, close_mode: str = "always") -> str:
             "close_event": close_event,
             "trace_path": trace_path,
             "trace_ready": False,
+            "vars": {},
         }

-    def on_finished(error: Exception | None) -> None:
+    def on_finished(error: Exception | None, vars_: dict) -> None:
         with _LOCK:
             _RUNS[run_id]["status"] = "error" if error else "done"
+            _RUNS[run_id]["vars"] = vars_

     def target() -> None:
         try:
@@ -69,6 +71,7 @@ def get_run(run_id: str) -> dict | None:
             "logs": list(run["logs"]),
             "browser_open": run["browser_open"],
             "trace_ready": run["trace_ready"],
+            "vars": run["vars"],
         }


diff --git a/webui/templates/index.html b/webui/templates/index.html
index 449925a..7a39cff 100644
--- a/webui/templates/index.html
+++ b/webui/templates/index.html
@@ -1,8 +1,8 @@
 <!DOCTYPE html>
-<html lang="sv">
+<html lang="en">
 <head>
 <meta charset="UTF-8">
-<title>Egenremiss RPA – flödesbyggare</title>
+<title>RPA Tool – flow builder</title>
 <style>
   :root {
     color-scheme: light dark;
@@ -85,15 +85,52 @@
     background: var(--panel);
   }
   .step-card.loop { background: var(--loop-bg); }
+  .step-card.step-disabled { opacity: 0.5; }
   .step-head {
     display: flex;
     align-items: center;
     gap: 8px;
     margin-bottom: 8px;
   }
+  .step-head .enabled-toggle { flex: 0 0 auto; cursor: pointer; }
   .step-head .type-select { font-weight: 600; }
   .step-head .spacer { flex: 1; }
   .step-head button { padding: 3px 8px; font-size: 12px; }
+  .insert-divider {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+    height: 14px;
+    cursor: pointer;
+  }
+  .insert-divider .line {
+    position: absolute;
+    left: 0;
+    right: 0;
+    top: 50%;
+    border-top: 1px dashed transparent;
+  }
+  .insert-divider:hover .line { border-top: 1px dashed var(--accent); }
+  .insert-divider .plus {
+    position: relative;
+    width: 16px;
+    height: 16px;
+    line-height: 14px;
+    text-align: center;
+    border-radius: 50%;
+    border: 1px solid var(--border);
+    background: var(--panel);
+    color: var(--muted);
+    font-size: 12px;
+    opacity: 0;
+    transition: opacity 0.1s;
+  }
+  .insert-divider:hover .plus {
+    opacity: 1;
+    color: var(--accent);
+    border-color: var(--accent);
+  }
   .fields {
     display: grid;
     grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
@@ -114,6 +151,14 @@
   }
   .nested .steps { margin-bottom: 8px; }
   .add-step-row { margin-top: 4px; }
+  .branch {
+    border: 1px dashed var(--border);
+    border-radius: 6px;
+    padding: 8px;
+    margin-bottom: 8px;
+  }
+  .branch .step-head { margin-bottom: 6px; }
+  .branch.else-branch { border-style: solid; }
   #log {
     background: #111114;
     color: #d6d6d8;
@@ -136,147 +181,417 @@
   .status-done { background: #3aa757; color: #fff; }
   .status-error { background: var(--danger); color: #fff; }
   .hint { color: var(--muted); font-size: 12px; margin: 0 0 10px; }
+  .side-col { display: flex; flex-direction: column; gap: 16px; }
+  .var-row {
+    border: 1px solid var(--border);
+    border-radius: 6px;
+    margin-bottom: 6px;
+    overflow: hidden;
+  }
+  .var-header {
+    padding: 6px 10px;
+    cursor: pointer;
+    font-family: ui-monospace, Consolas, monospace;
+    font-size: 12px;
+    background: var(--bg);
+  }
+  .var-header:hover { background: var(--loop-bg); }
+  .var-header .var-type { color: var(--muted); font-weight: normal; }
+  .var-value {
+    margin: 0;
+    padding: 8px 10px;
+    background: #111114;
+    color: #d6d6d8;
+    font-family: ui-monospace, Consolas, monospace;
+    font-size: 12px;
+    max-height: 240px;
+    overflow: auto;
+    white-space: pre-wrap;
+  }
 </style>
 </head>
 <body>

 <header>
-  <h1>Egenremiss RPA</h1>
+  <h1 id="appTitle">RPA Tool</h1>
+  <select id="langSelect">
+    <option value="en">English</option>
+    <option value="sv">Svenska</option>
+  </select>
   <select id="flowSelect"></select>
-  <button class="secondary" id="newFlowBtn">Nytt flöde</button>
-  <button class="danger" id="deleteFlowBtn">Ta bort flöde</button>
+  <button class="secondary" id="newFlowBtn">New flow</button>
+  <button class="danger" id="deleteFlowBtn">Delete flow</button>
   <span class="spacer" style="flex:1"></span>
-  <select id="closeModeSelect" title="Vad ska hända med webbläsaren när flödet är klart?">
-    <option value="always">Stäng webbläsaren</option>
-    <option value="on_success">Håll öppen vid fel</option>
-    <option value="never">Håll alltid öppen</option>
+  <select id="closeModeSelect">
+    <option value="always">Close the browser</option>
+    <option value="on_success">Keep open on error</option>
+    <option value="never">Always keep open</option>
   </select>
-  <button class="secondary" id="saveBtn">Spara</button>
-  <button id="runBtn">Kör flöde</button>
-  <button class="secondary" id="closeBrowserBtn" style="display:none">Stäng webbläsare</button>
-  <button class="secondary" id="showTraceBtn" style="display:none">Visa trace</button>
+  <button class="secondary" id="saveBtn">Save</button>
+  <button id="runBtn">Run flow</button>
+  <button class="secondary" id="closeBrowserBtn" style="display:none">Close browser</button>
+  <button class="secondary" id="showTraceBtn" style="display:none">Show trace</button>
   <span id="statusBadge"></span>
 </header>

 <main>
   <div class="panel">
-    <p class="hint">
-      Bygg flödet steg för steg. "Lista poster" hittar element via en CSS-selector
-      och sparar dem i en variabel; "Loop" itererar över den variabeln. Inuti en
-      loop kan Sökområde sättas till "item" för att bara söka inom aktuell post.
-    </p>
+    <p class="hint" id="stepsHint"></p>
     <div id="rootSteps" class="steps"></div>
     <div class="add-step-row">
-      <button class="secondary" id="addRootStepBtn">+ Lägg till steg</button>
+      <button class="secondary" id="addRootStepBtn">+ Add step</button>
     </div>
   </div>

-  <div class="panel">
-    <strong>Körlogg</strong>
-    <div id="log"></div>
+  <div class="side-col">
+    <div class="panel">
+      <strong id="logTitle">Run log</strong>
+      <div id="log"></div>
+    </div>
+    <div class="panel">
+      <strong id="varsTitle">Variables</strong>
+      <p class="hint" id="varsHint"></p>
+      <div id="varsPanel"></div>
+    </div>
   </div>
 </main>

 <script>
+// ---------------------------------------------------------------------
+// i18n: English is the default UI language; Swedish is a full translation
+// selectable via the language dropdown (persisted in localStorage). Only
+// the browser UI is translated here - the run log is produced by the
+// Python backend and stays in Swedish regardless of the UI language.
+// ---------------------------------------------------------------------
+
+const I18N = {
+  en: {
+    ui_title: "RPA Tool",
+    ui_page_title: "RPA Tool – flow builder",
+    ui_new_flow: "New flow",
+    ui_delete_flow: "Delete flow",
+    ui_close_mode_title: "What should happen to the browser when the flow finishes?",
+    ui_close_always: "Close the browser",
+    ui_close_on_success: "Keep open on error",
+    ui_close_never: "Always keep open",
+    ui_save: "Save",
+    ui_run: "Run flow",
+    ui_close_browser_btn: "Close browser",
+    ui_show_trace: "Show trace",
+    ui_steps_hint: 'Build the flow step by step. "List items" finds elements via a CSS selector and saves them in a variable; "Loop" iterates over that variable. Inside a loop, Search scope can be set to "item" to search only within the current item.',
+    ui_add_step: "+ Add step",
+    ui_run_log: "Run log",
+    ui_variables: "Variables",
+    ui_variables_hint: "Click a variable name to see its content (filled in once the flow finishes).",
+    ui_no_vars_yet: "No variables yet.",
+    ui_insert_step_here: "Insert step here",
+    ui_label_placeholder: "Label (optional)",
+    ui_label_title: "Name this step so a 'Jump to label' step can jump here",
+    ui_enabled_title: "Enabled (unchecked = step is skipped when running)",
+    ui_remove: "Remove",
+    ui_add_step_in_loop: "+ Add step in loop",
+    ui_branch_if: "If",
+    ui_branch_else_if: "...else if",
+    ui_remove_branch: "Remove branch",
+    ui_add_else_if: "+ Add 'else if' branch",
+    ui_else_toggle: "...else",
+    ui_condition_label: "Condition",
+    ui_status_running: "Running…",
+    ui_status_done: "Done",
+    ui_status_error: "Error",
+    ui_var_list: "list, {n} item(s)",
+    ui_var_object: "object",
+    ui_var_text: "text, {n} characters",
+    ui_new_flow_prompt: "Name of new flow:",
+    ui_delete_flow_confirm: 'Delete the flow "{name}"?',
+
+    step_goto: "Go to URL",
+    step_wait: "Wait (seconds)",
+    step_wait_for_load: "Wait for page to load",
+    step_wait_for_login: "Wait for login",
+    step_wait_for_selector: "Wait for element",
+    step_click: "Click",
+    step_type: "Type in field",
+    step_press_key: "Press key",
+    step_select_option: "Select in dropdown",
+    step_check: "Check checkbox/radio",
+    step_extract_text: "Read text into variable",
+    step_set_var: "Set variable",
+    step_list_append: "Append to list",
+    step_save_var: "Save variable (JSON/CSV)",
+    step_load_var: "Load variable (JSON/CSV)",
+    step_run_script: "Run Python script",
+    step_screenshot: "Take screenshot",
+    step_list: "List items",
+    step_if: "If / else if / else",
+    step_jump_to_label: "Jump to label",
+    step_loop: "Loop (for each item)",
+    step_download: "Download",
+
+    cond_element_exists: "Element exists",
+    cond_element_not_exists: "Element does not exist",
+    cond_var_equals: "Variable equals",
+    cond_var_not_equals: "Variable does NOT equal",
+    cond_var_contains: "Variable contains",
+
+    f_by: "Find by",
+    f_value: "Text / CSS selector / role name",
+    f_role: "Role (if by=role, e.g. link, button)",
+    f_scope: "Search scope",
+    f_nth: "Index (0 = first match)",
+
+    condf_var_name: "Variable name",
+    condf_compare_text: "Compare with",
+    condf_contains_text: "Should contain",
+
+    fd_goto_url: "URL",
+    fd_wait_seconds: "Seconds",
+    fd_timeout_seconds: "Timeout (seconds)",
+    fd_type_text: "Text to type",
+    fd_press_key_key: "Key (e.g. Enter, Tab, Escape)",
+    fd_press_key_value: "Text / CSS selector / role name (empty = whole page)",
+    fd_by_conditional: "Find by (if the above is filled in)",
+    fd_role_short: "Role (if by=role)",
+    fd_select_option_option: "Option (visible text)",
+    fd_check_checked: "Check (unchecked = uncheck)",
+    fd_save_as_var: "Save as variable",
+    fd_var_name: "Variable name",
+    fd_set_var_text: "Value",
+    fd_list_append_var: "List variable",
+    fd_list_append_text: "Literal value (used if the selector below is empty)",
+    fd_list_append_value: "Or: Text / CSS selector / role name (reads from the page)",
+    fd_save_var_var: "Variable to save",
+    fd_export_filename: "Filename in data/exports/ (.json or .csv)",
+    fd_save_var_format: "Format (empty = guess from filename)",
+    fd_save_var_mode: "Mode",
+    fd_run_script_path: "Path to Python script",
+    fd_run_script_args: "Arguments (space-separated)",
+    fd_run_script_var: "Save stdout as variable (optional, JSON if possible)",
+    fd_screenshot_name: "Filename (without .png)",
+    fd_list_value: "CSS selector",
+    fd_loop_over: "Loop over variable",
+    fd_jump_target: "Jump to label",
+  },
+  sv: {
+    ui_title: "RPA-verktyg",
+    ui_page_title: "RPA-verktyg – flödesbyggare",
+    ui_new_flow: "Nytt flöde",
+    ui_delete_flow: "Ta bort flöde",
+    ui_close_mode_title: "Vad ska hända med webbläsaren när flödet är klart?",
+    ui_close_always: "Stäng webbläsaren",
+    ui_close_on_success: "Håll öppen vid fel",
+    ui_close_never: "Håll alltid öppen",
+    ui_save: "Spara",
+    ui_run: "Kör flöde",
+    ui_close_browser_btn: "Stäng webbläsare",
+    ui_show_trace: "Visa trace",
+    ui_steps_hint: 'Bygg flödet steg för steg. "Lista poster" hittar element via en CSS-selector och sparar dem i en variabel; "Loop" itererar över den variabeln. Inuti en loop kan Sökområde sättas till "item" för att bara söka inom aktuell post.',
+    ui_add_step: "+ Lägg till steg",
+    ui_run_log: "Körlogg",
+    ui_variables: "Variabler",
+    ui_variables_hint: "Klicka på ett variabelnamn för att se dess innehåll (fylls i när flödet är klart).",
+    ui_no_vars_yet: "Inga variabler ännu.",
+    ui_insert_step_here: "Infoga steg här",
+    ui_label_placeholder: "Etikett (valfri)",
+    ui_label_title: "Namnge steget så ett 'Hoppa till etikett'-steg kan hoppa hit",
+    ui_enabled_title: "Aktiverat (avbockad = steget hoppas över vid körning)",
+    ui_remove: "Ta bort",
+    ui_add_step_in_loop: "+ Lägg till steg i loop",
+    ui_branch_if: "Om",
+    ui_branch_else_if: "...annars om",
+    ui_remove_branch: "Ta bort gren",
+    ui_add_else_if: "+ Lägg till 'else if'-gren",
+    ui_else_toggle: "...annars (else)",
+    ui_condition_label: "Villkor",
+    ui_status_running: "Kör...",
+    ui_status_done: "Klar",
+    ui_status_error: "Fel",
+    ui_var_list: "lista, {n} post(er)",
+    ui_var_object: "objekt",
+    ui_var_text: "text, {n} tecken",
+    ui_new_flow_prompt: "Namn på nytt flöde:",
+    ui_delete_flow_confirm: 'Ta bort flödet "{name}"?',
+
+    step_goto: "Gå till URL",
+    step_wait: "Vänta (sekunder)",
+    step_wait_for_load: "Vänta på att sidan laddas",
+    step_wait_for_login: "Vänta på inloggning",
+    step_wait_for_selector: "Vänta på element",
+    step_click: "Klicka",
+    step_type: "Skriv i textfält",
+    step_press_key: "Tryck tangent",
+    step_select_option: "Välj i dropdown",
+    step_check: "Kryssa checkbox/radio",
+    step_extract_text: "Läs text till variabel",
+    step_set_var: "Sätt variabel",
+    step_list_append: "Lägg till i lista",
+    step_save_var: "Spara variabel (JSON/CSV)",
+    step_load_var: "Läs in variabel (JSON/CSV)",
+    step_run_script: "Kör Python-script",
+    step_screenshot: "Ta skärmdump",
+    step_list: "Lista poster",
+    step_if: "Om / annars om / annars",
+    step_jump_to_label: "Hoppa till etikett",
+    step_loop: "Loop (för varje post)",
+    step_download: "Ladda ner",
+
+    cond_element_exists: "Element finns",
+    cond_element_not_exists: "Element finns inte",
+    cond_var_equals: "Variabel är lika med",
+    cond_var_not_equals: "Variabel är INTE lika med",
+    cond_var_contains: "Variabel innehåller",
+
+    f_by: "Hitta via",
+    f_value: "Text / CSS-selector / rollnamn",
+    f_role: "Roll (om by=role, ex. link, button)",
+    f_scope: "Sökområde",
+    f_nth: "Index (0 = första matchning)",
+
+    condf_var_name: "Variabelnamn",
+    condf_compare_text: "Jämför med",
+    condf_contains_text: "Ska innehålla",
+
+    fd_goto_url: "URL",
+    fd_wait_seconds: "Sekunder",
+    fd_timeout_seconds: "Timeout (sekunder)",
+    fd_type_text: "Text att skriva",
+    fd_press_key_key: "Tangent (ex. Enter, Tab, Escape)",
+    fd_press_key_value: "Text / CSS-selector / rollnamn (tomt = hela sidan)",
+    fd_by_conditional: "Hitta via (om ovan ifylld)",
+    fd_role_short: "Roll (om by=role)",
+    fd_select_option_option: "Alternativ (synlig text)",
+    fd_check_checked: "Kryssa i (av = kryssa ur)",
+    fd_save_as_var: "Spara som variabel",
+    fd_var_name: "Variabelnamn",
+    fd_set_var_text: "Värde",
+    fd_list_append_var: "Listvariabel",
+    fd_list_append_text: "Literalt värde (används om selector nedan är tom)",
+    fd_list_append_value: "Eller: Text / CSS-selector / rollnamn (hämtar från sidan)",
+    fd_save_var_var: "Variabel att spara",
+    fd_export_filename: "Filnamn i data/exports/ (.json eller .csv)",
+    fd_save_var_format: "Format (tomt = gissa från filnamn)",
+    fd_save_var_mode: "Läge",
+    fd_run_script_path: "Sökväg till Python-script",
+    fd_run_script_args: "Argument (mellanslagsseparerade)",
+    fd_run_script_var: "Spara stdout som variabel (valfritt, JSON om möjligt)",
+    fd_screenshot_name: "Filnamn (utan .png)",
+    fd_list_value: "CSS-selector",
+    fd_loop_over: "Loopa över variabel",
+    fd_jump_target: "Hoppa till etikett",
+  },
+};
+
+let LANG = localStorage.getItem("rpa_lang") || "en";
+
+function t(key) {
+  return (I18N[LANG] && I18N[LANG][key]) ?? (I18N.en && I18N.en[key]) ?? key;
+}
+
+function tf(key, params) {
+  let s = t(key);
+  for (const [k, v] of Object.entries(params || {})) {
+    s = s.replace(`{${k}}`, v);
+  }
+  return s;
+}
+
 const STEP_TYPES = [
   "goto", "wait", "wait_for_load", "wait_for_login", "wait_for_selector",
   "click", "type", "press_key", "select_option", "check",
   "extract_text", "set_var", "list_append", "save_var", "load_var", "run_script",
-  "screenshot", "list", "loop", "download",
+  "screenshot", "list", "if", "jump_to_label", "loop", "download",
 ];

-const STEP_LABELS = {
-  goto: "Gå till URL",
-  wait: "Vänta (sekunder)",
-  wait_for_load: "Vänta på att sidan laddas",
-  wait_for_login: "Vänta på inloggning (BankID)",
-  wait_for_selector: "Vänta på element",
-  click: "Klicka",
-  type: "Skriv i textfält",
-  press_key: "Tryck tangent",
-  select_option: "Välj i dropdown",
-  check: "Kryssa checkbox/radio",
-  extract_text: "Läs text till variabel",
-  set_var: "Sätt variabel",
-  list_append: "Lägg till i lista",
-  save_var: "Spara variabel (JSON/CSV)",
-  load_var: "Läs in variabel (JSON/CSV)",
-  run_script: "Kör Python-script",
-  screenshot: "Ta skärmdump",
-  list: "Lista poster",
-  loop: "Loop (för varje post)",
-  download: "Ladda ner",
-};
-
 const SELECTOR_FIELDS = [
-  { key: "by", label: "Hitta via", type: "select", options: ["text", "css", "role"], default: "text" },
-  { key: "value", label: "Text / CSS-selector / rollnamn", type: "text" },
-  { key: "role", label: "Roll (om by=role, ex. link, button)", type: "text" },
-  { key: "scope", label: "Sökområde", type: "select", options: ["page", "item"], default: "page" },
-  { key: "nth", label: "Index (0 = första matchning)", type: "number", default: 0 },
+  { key: "by", labelKey: "f_by", type: "select", options: ["text", "css", "role"], default: "text" },
+  { key: "value", labelKey: "f_value", type: "text" },
+  { key: "role", labelKey: "f_role", type: "text" },
+  { key: "scope", labelKey: "f_scope", type: "select", options: ["page", "item"], default: "page" },
+  { key: "nth", labelKey: "f_nth", type: "number", default: 0 },
 ];

+const CONDITION_KINDS = ["element_exists", "element_not_exists", "var_equals", "var_not_equals", "var_contains"];
+
+const CONDITION_FIELD_DEFS = {
+  element_exists: SELECTOR_FIELDS,
+  element_not_exists: SELECTOR_FIELDS,
+  var_equals: [
+    { key: "var", labelKey: "condf_var_name", type: "text" },
+    { key: "text", labelKey: "condf_compare_text", type: "text" },
+  ],
+  var_not_equals: [
+    { key: "var", labelKey: "condf_var_name", type: "text" },
+    { key: "text", labelKey: "condf_compare_text", type: "text" },
+  ],
+  var_contains: [
+    { key: "var", labelKey: "condf_var_name", type: "text" },
+    { key: "text", labelKey: "condf_contains_text", type: "text" },
+  ],
+};
+
 const FIELD_DEFS = {
-  goto: [{ key: "url", label: "URL", type: "text" }],
-  wait: [{ key: "seconds", label: "Sekunder", type: "number", default: 1 }],
+  goto: [{ key: "url", labelKey: "fd_goto_url", type: "text" }],
+  wait: [{ key: "seconds", labelKey: "fd_wait_seconds", type: "number", default: 1 }],
   wait_for_load: [],
-  wait_for_login: [{ key: "timeout", label: "Timeout (sekunder)", type: "number", default: 180 }],
+  wait_for_login: [{ key: "timeout", labelKey: "fd_timeout_seconds", type: "number", default: 180 }],
   wait_for_selector: [
     ...SELECTOR_FIELDS,
-    { key: "timeout", label: "Timeout (sekunder)", type: "number", default: 15 },
+    { key: "timeout", labelKey: "fd_timeout_seconds", type: "number", default: 15 },
   ],
   click: SELECTOR_FIELDS,
-  type: [...SELECTOR_FIELDS, { key: "text", label: "Text att skriva", type: "text" }],
+  type: [...SELECTOR_FIELDS, { key: "text", labelKey: "fd_type_text", type: "text" }],
   press_key: [
-    { key: "key", label: "Tangent (ex. Enter, Tab, Escape)", type: "text", default: "Enter" },
-    { key: "value", label: "Text / CSS-selector / rollnamn (tomt = hela sidan)", type: "text" },
-    { key: "by", label: "Hitta via (om ovan ifylld)", type: "select", options: ["text", "css", "role"], default: "text" },
-    { key: "role", label: "Roll (om by=role)", type: "text" },
-    { key: "scope", label: "Sökområde", type: "select", options: ["page", "item"], default: "page" },
-    { key: "nth", label: "Index (0 = första matchning)", type: "number", default: 0 },
+    { key: "key", labelKey: "fd_press_key_key", type: "text", default: "Enter" },
+    { key: "value", labelKey: "fd_press_key_value", type: "text" },
+    { key: "by", labelKey: "fd_by_conditional", type: "select", options: ["text", "css", "role"], default: "text" },
+    { key: "role", labelKey: "fd_role_short", type: "text" },
+    { key: "scope", labelKey: "f_scope", type: "select", options: ["page", "item"], default: "page" },
+    { key: "nth", labelKey: "f_nth", type: "number", default: 0 },
   ],
-  select_option: [...SELECTOR_FIELDS, { key: "option", label: "Alternativ (synlig text)", type: "text" }],
+  select_option: [...SELECTOR_FIELDS, { key: "option", labelKey: "fd_select_option_option", type: "text" }],
   check: [
     ...SELECTOR_FIELDS,
-    { key: "checked", label: "Kryssa i (av = kryssa ur)", type: "select", options: ["true", "false"], default: "true" },
+    { key: "checked", labelKey: "fd_check_checked", type: "select", options: ["true", "false"], default: "true" },
   ],
-  extract_text: [...SELECTOR_FIELDS, { key: "var", label: "Spara som variabel", type: "text" }],
+  extract_text: [...SELECTOR_FIELDS, { key: "var", labelKey: "fd_save_as_var", type: "text" }],
   set_var: [
-    { key: "var", label: "Variabelnamn", type: "text" },
-    { key: "text", label: "Värde", type: "text" },
+    { key: "var", labelKey: "fd_var_name", type: "text" },
+    { key: "text", labelKey: "fd_set_var_text", type: "text" },
   ],
   list_append: [
-    { key: "var", label: "Listvariabel", type: "text" },
-    { key: "text", label: "Literalt värde (används om selector nedan är tom)", type: "text" },
-    { key: "value", label: "Eller: Text / CSS-selector / rollnamn (hämtar från sidan)", type: "text" },
-    { key: "by", label: "Hitta via (om ovan ifylld)", type: "select", options: ["text", "css", "role"], default: "text" },
-    { key: "role", label: "Roll (om by=role)", type: "text" },
-    { key: "scope", label: "Sökområde", type: "select", options: ["page", "item"], default: "page" },
-    { key: "nth", label: "Index (0 = första matchning)", type: "number", default: 0 },
+    { key: "var", labelKey: "fd_list_append_var", type: "text" },
+    { key: "text", labelKey: "fd_list_append_text", type: "text" },
+    { key: "value", labelKey: "fd_list_append_value", type: "text" },
+    { key: "by", labelKey: "fd_by_conditional", type: "select", options: ["text", "css", "role"], default: "text" },
+    { key: "role", labelKey: "fd_role_short", type: "text" },
+    { key: "scope", labelKey: "f_scope", type: "select", options: ["page", "item"], default: "page" },
+    { key: "nth", labelKey: "f_nth", type: "number", default: 0 },
   ],
   save_var: [
-    { key: "var", label: "Variabel att spara", type: "text" },
-    { key: "filename", label: "Filnamn i data/exports/ (.json eller .csv)", type: "text" },
-    { key: "format", label: "Format (tomt = gissa från filnamn)", type: "select", options: ["", "json", "csv"], default: "" },
-    { key: "mode", label: "Läge", type: "select", options: ["overwrite", "append"], default: "overwrite" },
+    { key: "var", labelKey: "fd_save_var_var", type: "text" },
+    { key: "filename", labelKey: "fd_export_filename", type: "text" },
+    { key: "format", labelKey: "fd_save_var_format", type: "select", options: ["", "json", "csv"], default: "" },
+    { key: "mode", labelKey: "fd_save_var_mode", type: "select", options: ["overwrite", "append"], default: "overwrite" },
   ],
   load_var: [
-    { key: "var", label: "Spara som variabel", type: "text" },
-    { key: "filename", label: "Filnamn i data/exports/ (.json eller .csv)", type: "text" },
+    { key: "var", labelKey: "fd_save_as_var", type: "text" },
+    { key: "filename", labelKey: "fd_export_filename", type: "text" },
   ],
   run_script: [
-    { key: "path", label: "Sökväg till Python-script", type: "text" },
-    { key: "args", label: "Argument (mellanslagsseparerade)", type: "text" },
-    { key: "timeout", label: "Timeout (sekunder)", type: "number", default: 60 },
-    { key: "var", label: "Spara stdout som variabel (valfritt, JSON om möjligt)", type: "text" },
+    { key: "path", labelKey: "fd_run_script_path", type: "text" },
+    { key: "args", labelKey: "fd_run_script_args", type: "text" },
+    { key: "timeout", labelKey: "fd_timeout_seconds", type: "number", default: 60 },
+    { key: "var", labelKey: "fd_run_script_var", type: "text" },
   ],
-  screenshot: [{ key: "name", label: "Filnamn (utan .png)", type: "text", default: "screenshot" }],
+  screenshot: [{ key: "name", labelKey: "fd_screenshot_name", type: "text", default: "screenshot" }],
   list: [
-    { key: "value", label: "CSS-selector", type: "text" },
-    { key: "var", label: "Spara som variabel", type: "text" },
+    { key: "value", labelKey: "fd_list_value", type: "text" },
+    { key: "var", labelKey: "fd_save_as_var", type: "text" },
   ],
-  loop: [{ key: "over", label: "Loopa över variabel", type: "text" }],
+  loop: [{ key: "over", labelKey: "fd_loop_over", type: "text" }],
   download: SELECTOR_FIELDS,
+  if: [],
+  jump_to_label: [{ key: "target", labelKey: "fd_jump_target", type: "text" }],
 };

 let currentFlowName = null;
@@ -289,6 +604,7 @@ function newStep(type) {
     if (f.default !== undefined) step[f.key] = f.default;
   }
   if (type === "loop") step.steps = [];
+  if (type === "if") step.branches = [{ condition: { kind: "element_exists" }, steps: [] }];
   return step;
 }

@@ -296,7 +612,7 @@ function fieldRow(step, def, onStructuralChange) {
   const wrap = document.createElement("div");
   wrap.className = "field";
   const label = document.createElement("label");
-  label.textContent = def.label;
+  label.textContent = t(def.labelKey);
   wrap.appendChild(label);

   let input;
@@ -321,19 +637,61 @@ function fieldRow(step, def, onStructuralChange) {
   return wrap;
 }

+function renderCondition(condition, onStructuralChange) {
+  const wrap = document.createElement("div");
+  wrap.className = "fields";
+
+  const kindWrap = document.createElement("div");
+  kindWrap.className = "field";
+  const kindLabel = document.createElement("label");
+  kindLabel.textContent = t("ui_condition_label");
+  kindWrap.appendChild(kindLabel);
+  const kindSelect = document.createElement("select");
+  for (const k of CONDITION_KINDS) {
+    const o = document.createElement("option");
+    o.value = k;
+    o.textContent = t("cond_" + k);
+    kindSelect.appendChild(o);
+  }
+  kindSelect.value = condition.kind || "element_exists";
+  kindSelect.addEventListener("change", () => {
+    for (const key of Object.keys(condition)) delete condition[key];
+    condition.kind = kindSelect.value;
+    onStructuralChange();
+  });
+  kindWrap.appendChild(kindSelect);
+  wrap.appendChild(kindWrap);
+
+  for (const def of CONDITION_FIELD_DEFS[condition.kind || "element_exists"] || []) {
+    wrap.appendChild(fieldRow(condition, def, onStructuralChange));
+  }
+  return wrap;
+}
+
 function renderStepCard(step, arr, idx, isItemScope, onStructuralChange) {
   const card = document.createElement("div");
-  card.className = "step-card" + (step.type === "loop" ? " loop" : "");
+  card.className = "step-card" + (step.type === "loop" ? " loop" : "") + (step.enabled === false ? " step-disabled" : "");

   const head = document.createElement("div");
   head.className = "step-head";

+  const enabledToggle = document.createElement("input");
+  enabledToggle.type = "checkbox";
+  enabledToggle.className = "enabled-toggle";
+  enabledToggle.title = t("ui_enabled_title");
+  enabledToggle.checked = step.enabled !== false;
+  enabledToggle.addEventListener("change", () => {
+    step.enabled = enabledToggle.checked;
+    card.classList.toggle("step-disabled", !enabledToggle.checked);
+  });
+  head.appendChild(enabledToggle);
+
   const typeSelect = document.createElement("select");
   typeSelect.className = "type-select";
-  for (const t of STEP_TYPES) {
+  for (const stepType of STEP_TYPES) {
     const o = document.createElement("option");
-    o.value = t;
-    o.textContent = STEP_LABELS[t];
+    o.value = stepType;
+    o.textContent = t("step_" + stepType);
     typeSelect.appendChild(o);
   }
   typeSelect.value = step.type;
@@ -344,6 +702,17 @@ function renderStepCard(step, arr, idx, isItemScope, onStructuralChange) {
   });
   head.appendChild(typeSelect);

+  const labelInput = document.createElement("input");
+  labelInput.type = "text";
+  labelInput.placeholder = t("ui_label_placeholder");
+  labelInput.title = t("ui_label_title");
+  labelInput.style.width = "120px";
+  labelInput.value = step.label || "";
+  labelInput.addEventListener("input", () => {
+    step.label = labelInput.value || undefined;
+  });
+  head.appendChild(labelInput);
+
   head.appendChild(document.createElement("span")).className = "spacer";

   const upBtn = document.createElement("button");
@@ -366,7 +735,7 @@ function renderStepCard(step, arr, idx, isItemScope, onStructuralChange) {

   const delBtn = document.createElement("button");
   delBtn.className = "danger";
-  delBtn.textContent = "Ta bort";
+  delBtn.textContent = t("ui_remove");
   delBtn.addEventListener("click", () => {
     arr.splice(idx, 1);
     onStructuralChange();
@@ -398,7 +767,7 @@ function renderStepCard(step, arr, idx, isItemScope, onStructuralChange) {

     const addBtn = document.createElement("button");
     addBtn.className = "secondary add-step-row";
-    addBtn.textContent = "+ Lägg till steg i loop";
+    addBtn.textContent = t("ui_add_step_in_loop");
     addBtn.addEventListener("click", () => {
       step.steps.push(newStep("click"));
       onStructuralChange();
@@ -407,13 +776,117 @@ function renderStepCard(step, arr, idx, isItemScope, onStructuralChange) {
     card.appendChild(nested);
   }

+  if (step.type === "if") {
+    if (!Array.isArray(step.branches) || step.branches.length === 0) {
+      step.branches = [{ condition: { kind: "element_exists" }, steps: [] }];
+    }
+
+    const ifBlock = document.createElement("div");
+    ifBlock.className = "nested";
+
+    step.branches.forEach((branch, bIdx) => {
+      if (!branch.condition) branch.condition = { kind: "element_exists" };
+      if (!Array.isArray(branch.steps)) branch.steps = [];
+
+      const branchWrap = document.createElement("div");
+      branchWrap.className = "branch";
+
+      const branchHead = document.createElement("div");
+      branchHead.className = "step-head";
+      const branchTitle = document.createElement("strong");
+      branchTitle.textContent = bIdx === 0 ? t("ui_branch_if") : t("ui_branch_else_if");
+      branchHead.appendChild(branchTitle);
+      branchHead.appendChild(document.createElement("span")).className = "spacer";
+      if (bIdx > 0) {
+        const removeBranchBtn = document.createElement("button");
+        removeBranchBtn.className = "danger";
+        removeBranchBtn.textContent = t("ui_remove_branch");
+        removeBranchBtn.addEventListener("click", () => {
+          step.branches.splice(bIdx, 1);
+          onStructuralChange();
+        });
+        branchHead.appendChild(removeBranchBtn);
+      }
+      branchWrap.appendChild(branchHead);
+
+      branchWrap.appendChild(renderCondition(branch.condition, onStructuralChange));
+
+      const branchSteps = document.createElement("div");
+      branchSteps.className = "steps";
+      renderStepsInto(branchSteps, branch.steps, isItemScope, onStructuralChange);
+      branchWrap.appendChild(branchSteps);
+
+      ifBlock.appendChild(branchWrap);
+    });
+
+    const addBranchBtn = document.createElement("button");
+    addBranchBtn.className = "secondary add-step-row";
+    addBranchBtn.textContent = t("ui_add_else_if");
+    addBranchBtn.addEventListener("click", () => {
+      step.branches.push({ condition: { kind: "element_exists" }, steps: [] });
+      onStructuralChange();
+    });
+    ifBlock.appendChild(addBranchBtn);
+
+    const elseWrap = document.createElement("div");
+    elseWrap.className = "branch else-branch";
+    const elseHead = document.createElement("div");
+    elseHead.className = "step-head";
+    const elseToggleLabel = document.createElement("label");
+    elseToggleLabel.style.fontSize = "13px";
+    elseToggleLabel.style.display = "flex";
+    elseToggleLabel.style.alignItems = "center";
+    elseToggleLabel.style.gap = "6px";
+    const elseCheckbox = document.createElement("input");
+    elseCheckbox.type = "checkbox";
+    elseCheckbox.checked = Array.isArray(step.else);
+    elseCheckbox.addEventListener("change", () => {
+      step.else = elseCheckbox.checked ? [] : undefined;
+      onStructuralChange();
+    });
+    elseToggleLabel.appendChild(elseCheckbox);
+    elseToggleLabel.appendChild(document.createTextNode(t("ui_else_toggle")));
+    elseHead.appendChild(elseToggleLabel);
+    elseWrap.appendChild(elseHead);
+
+    if (Array.isArray(step.else)) {
+      const elseSteps = document.createElement("div");
+      elseSteps.className = "steps";
+      renderStepsInto(elseSteps, step.else, isItemScope, onStructuralChange);
+      elseWrap.appendChild(elseSteps);
+    }
+    ifBlock.appendChild(elseWrap);
+
+    card.appendChild(ifBlock);
+  }
+
   return card;
 }

+function insertDivider(arr, index, onStructuralChange) {
+  const div = document.createElement("div");
+  div.className = "insert-divider";
+  div.title = t("ui_insert_step_here");
+  const line = document.createElement("div");
+  line.className = "line";
+  div.appendChild(line);
+  const plus = document.createElement("div");
+  plus.className = "plus";
+  plus.textContent = "+";
+  div.appendChild(plus);
+  div.addEventListener("click", () => {
+    arr.splice(index, 0, newStep("click"));
+    onStructuralChange();
+  });
+  return div;
+}
+
 function renderStepsInto(container, arr, isItemScope, onStructuralChange) {
   container.innerHTML = "";
+  container.appendChild(insertDivider(arr, 0, onStructuralChange));
   arr.forEach((step, idx) => {
     container.appendChild(renderStepCard(step, arr, idx, isItemScope, onStructuralChange));
+    container.appendChild(insertDivider(arr, idx + 1, onStructuralChange));
   });
 }

@@ -468,7 +941,7 @@ document.getElementById("flowSelect").addEventListener("change", (e) => {
 });

 document.getElementById("newFlowBtn").addEventListener("click", async () => {
-  const name = prompt("Namn på nytt flöde:");
+  const name = prompt(t("ui_new_flow_prompt"));
   if (!name) return;
   currentFlowName = name;
   flow = { steps: [] };
@@ -479,7 +952,7 @@ document.getElementById("newFlowBtn").addEventListener("click", async () => {

 document.getElementById("deleteFlowBtn").addEventListener("click", async () => {
   if (!currentFlowName) return;
-  if (!confirm(`Ta bort flödet "${currentFlowName}"?`)) return;
+  if (!confirm(tf("ui_delete_flow_confirm", { name: currentFlowName }))) return;
   await fetch(`/api/flows/${encodeURIComponent(currentFlowName)}`, { method: "DELETE" });
   currentFlowName = null;
   const names = await loadFlowList();
@@ -495,6 +968,54 @@ const runBtn = document.getElementById("runBtn");
 const closeModeSelect = document.getElementById("closeModeSelect");
 const closeBrowserBtn = document.getElementById("closeBrowserBtn");
 const showTraceBtn = document.getElementById("showTraceBtn");
+const varsPanel = document.getElementById("varsPanel");
+const langSelect = document.getElementById("langSelect");
+const expandedVars = new Set();
+let lastStatus = null;
+
+function describeVarType(value) {
+  if (Array.isArray(value)) return tf("ui_var_list", { n: value.length });
+  if (value && typeof value === "object") return t("ui_var_object");
+  if (typeof value === "string") return tf("ui_var_text", { n: value.length });
+  return typeof value;
+}
+
+function renderVars(vars) {
+  varsPanel.innerHTML = "";
+  const names = Object.keys(vars || {});
+  if (!names.length) {
+    const p = document.createElement("p");
+    p.className = "hint";
+    p.textContent = t("ui_no_vars_yet");
+    varsPanel.appendChild(p);
+    return;
+  }
+  for (const name of names) {
+    const row = document.createElement("div");
+    row.className = "var-row";
+
+    const header = document.createElement("div");
+    header.className = "var-header";
+    header.innerHTML = `${name} <span class="var-type">(${describeVarType(vars[name])})</span>`;
+
+    const valueEl = document.createElement("pre");
+    valueEl.className = "var-value";
+    const value = vars[name];
+    valueEl.textContent = typeof value === "string" ? value : JSON.stringify(value, null, 2);
+    valueEl.style.display = expandedVars.has(name) ? "block" : "none";
+
+    header.addEventListener("click", () => {
+      const show = valueEl.style.display === "none";
+      valueEl.style.display = show ? "block" : "none";
+      if (show) expandedVars.add(name);
+      else expandedVars.delete(name);
+    });
+
+    row.appendChild(header);
+    row.appendChild(valueEl);
+    varsPanel.appendChild(row);
+  }
+}

 const savedCloseMode = localStorage.getItem("rpa_close_mode");
 if (savedCloseMode) closeModeSelect.value = savedCloseMode;
@@ -503,8 +1024,9 @@ closeModeSelect.addEventListener("change", () => {
 });

 function setStatus(status) {
+  lastStatus = status;
   statusBadge.className = "status-badge status-" + status;
-  statusBadge.textContent = { running: "Kör...", done: "Klar", error: "Fel" }[status] || "";
+  statusBadge.textContent = { running: t("ui_status_running"), done: t("ui_status_done"), error: t("ui_status_error") }[status] || "";
 }

 async function pollRun(runId) {
@@ -523,6 +1045,10 @@ async function pollRun(runId) {

   showTraceBtn.style.display = data.trace_ready ? "inline-block" : "none";

+  if (data.status !== "running") {
+    renderVars(data.vars);
+  }
+
   if (data.status === "running" || data.browser_open) {
     setTimeout(() => pollRun(runId), 1000);
   }
@@ -534,6 +1060,8 @@ document.getElementById("runBtn").addEventListener("click", async () => {
   runBtn.disabled = true;
   closeBrowserBtn.style.display = "none";
   showTraceBtn.style.display = "none";
+  expandedVars.clear();
+  renderVars({});
   setStatus("running");
   const res = await fetch(`/api/flows/${encodeURIComponent(currentFlowName)}/run`, {
     method: "POST",
@@ -554,6 +1082,46 @@ showTraceBtn.addEventListener("click", async () => {
   await fetch(`/api/runs/${encodeURIComponent(currentRunId)}/show-trace`, { method: "POST" });
 });

+function applyStaticTranslations() {
+  document.title = t("ui_page_title");
+  document.documentElement.lang = LANG;
+  document.getElementById("appTitle").textContent = t("ui_title");
+  document.getElementById("newFlowBtn").textContent = t("ui_new_flow");
+  document.getElementById("deleteFlowBtn").textContent = t("ui_delete_flow");
+  closeModeSelect.title = t("ui_close_mode_title");
+  closeModeSelect.options[0].textContent = t("ui_close_always");
+  closeModeSelect.options[1].textContent = t("ui_close_on_success");
+  closeModeSelect.options[2].textContent = t("ui_close_never");
+  document.getElementById("saveBtn").textContent = t("ui_save");
+  runBtn.textContent = t("ui_run");
+  closeBrowserBtn.textContent = t("ui_close_browser_btn");
+  showTraceBtn.textContent = t("ui_show_trace");
+  document.getElementById("stepsHint").textContent = t("ui_steps_hint");
+  document.getElementById("addRootStepBtn").textContent = t("ui_add_step");
+  document.getElementById("logTitle").textContent = t("ui_run_log");
+  document.getElementById("varsTitle").textContent = t("ui_variables");
+  document.getElementById("varsHint").textContent = t("ui_variables_hint");
+  if (lastStatus) setStatus(lastStatus);
+}
+
+langSelect.value = LANG;
+langSelect.addEventListener("change", () => {
+  LANG = langSelect.value;
+  localStorage.setItem("rpa_lang", LANG);
+  applyStaticTranslations();
+  render();
+  renderVars(lastRenderedVars);
+});
+
+let lastRenderedVars = {};
+const _renderVarsOrig = renderVars;
+renderVars = function (vars) {
+  lastRenderedVars = vars || {};
+  _renderVarsOrig(vars);
+};
+
+applyStaticTranslations();
+
 (async function init() {
   const names = await loadFlowList();
   if (names.length) {