Repository navigation
feat: plugin v2 — Svelte 5 rewrite, bulk operations, and a reorganised UI - #106
Merged
Merged
Conversation
CreateCopy (Phase 7):
- main/handlers/createCopy.ts: clones currentPage, rewrites connected
TextNodes with key labels (mode: keys) or per-language translations
(mode: languages), marks cloned pages with pageCopy: true via raw
setPluginData to avoid leaking marker into doc/global scope.
yields every 10 nodes, emits create-copy-progress
- writeTextSafely: hasMissingFont guard, mixed-font guard,
loadFontAsync over all ranges before mutating characters
- create-copy / create-copy-progress / create-copy-result messages
- CreateCopy.svelte: idle/fetching/creating/error stages, mode
picker (keys vs per-language), language checklist loaded lazily
from /v2/projects/languages, fetchAllTranslations builds the
\$\{ns\}|\$\{key\} -> text map for the main thread
- CopyView.svelte: read-only banner showing pageCopy state and
language, link back to settings
Canvas Annotations (Phase 8 — feature highlight):
- main/annotations/category.ts: ensureTolgeeCategory caches the id in
figma.root pluginData, recovers via label match across sessions,
creates a blue 'Tolgee' category if needed
- main/annotations/sync.ts: applyAnnotations / removeAnnotations /
syncCurrentPage / clearCurrentPage. Identical-write skip avoids
unnecessary co-editor churn. Uses findAllWithCriteria with the
tolgee_info pluginData filter for O(connected) scans. Dev mode
is a hard no-op (write-protected).
- main/annotations/reconcile.ts: 200ms debounced reconcile,
coalesces ids into a Set, resolves via getNodeByIdAsync
- main/annotations/index.ts: public API plus
isAnnotationsEnabled/setAnnotationsEnabled backed by clientStorage
(per-user, not per-document)
- handlers/annotations.ts: re-exports from $main/annotations
- main.ts: in-memory annotationsEnabled mirror so the synchronous
selectionchange / documentchange handlers don't pay an async cost.
selectionchange schedules a reconcile for the current selection.
currentpagechange triggers a full syncCurrentPage. documentchange
filters TEXT-node pluginData/characters/name changes and schedules
per-id reconciles. ui-ready dispatches command handling for
toggle-annotations / refresh-annotations and runs an initial
full sync if enabled. set-nodes-data and apply-translations also
kick the reconciler since same-plugin pluginData writes don't
always round-trip through documentchange.
- toggle-annotations / get-annotations-state / annotations-state
messages
Verification: 838 svelte-check files clean, 8/8 tests pass, build
produces dist/main.js + dist/ui.html (166 kB gzip) +
dist/ui-inspect.html + dist/manifest.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 9 — inspect panel finalisation: - ui-inspect/lib/deeplink.ts: buildKeyDeepLink + buildProjectDashboardLink with projectId-aware paths and graceful fallback when projectId is unknown. Strips trailing slashes - ui-inspect/App.svelte: brand header (Tolgee · language · project), empty-states with Plug / MousePointer lucide icons, per-node cards with Tag / Languages icons, ExternalLink / Copy / Check action row with 1s "Copied!" confirmation, footer with project deep-link, disabled-state when apiUrl is missing - shared/types.ts: projectId?: number on CurrentDocumentSettings - shared/messages.ts: persist-project-id UiToMain message - main/settings/merge.ts: projectId routed to document scope only - main/main.ts: persist-project-id handler writes to config and re-emits config-changed - SettingsSectionConnection.svelte: after successful Test Connection, sends persist-project-id so the inspect panel can build project-specific deep-links without re-validating the key Phase 10 — tests and bundle audit: Unit tests (87 passing, 1 skipped): - vitest.config.ts with shared/main/ui/inspect aliases mirroring tsconfig paths - 18 tests for keyFormat (5 casings, empty/special-char/missing placeholders) - 6 tests for ICU (plain, empty params fast-path, named substitution, plural one/other, malformed fallback) - 9 tests for settings merge (right-wins precedence, page-only, doc-only, global double-write, round-trip) - 13 tests for pushDiff (unchanged/changed/new/conflicts, plural mismatch, configured tags, keyless, characters fallback) - 12 tests for pullDiff (changed/missing/unchanged, disconnected skip, plural mismatch, namespace disambiguation) plus formatNodeText - 8 tests for findOutermostFrame with fake-tree casts - 13 tests for the new tolgeeFormat parser - deeplink.test.ts placeholder, now wired Bundle-size optimisation (550 kB raw / 166 kB gzip → 223 kB raw / 68 kB gzip, -59%): - replaced @tginternal/editor with a 130-line local ICU plural parser (tolgeeFormat.ts) — eliminates the @codemirror/view (376 kB) and @codemirror/state (142 kB) transitive deps. Bonus: pullDiff now extracts the real plural parameter name instead of assuming "count" - removed tailwind-merge and tailwind-variants — cn() collapses to clsx, button.svelte ships inline variant maps with identical consumer API - rollup-plugin-visualizer as ANALYZE=1 opt-in, both vite configs emit dist/stats-ui.html / dist/stats-ui-inspect.html on demand Final state: dist/main.js (16 kB), dist/ui.html (223 kB raw / 68 kB gzip), dist/ui-inspect.html (70 kB raw / 25 kB gzip), dist/manifest.json. typecheck + svelte-check + 87 tests all clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Figma's plugin iframe runs at origin "null" and chokes on <script type="module" crossorigin> — the crossorigin handshake fails against a null origin and the modulepreload polyfill Vite injects also tries to resolve preload candidates against that origin. Both of those break the UI before our code even runs, surfacing as a Syntax Error inside Figma's own vendor bundle when it tries to evaluate the malformed UI. Fix: disable modulePreload in both Vite configs and add a closeBundle plugin that strips type="module" and crossorigin from the inline script tag in dist/ui.html and dist/ui-inspect.html. The bundles contain no import/export statements (inlineDynamicImports is on and singlefile produces a flat IIFE-style module), so the classic script tag is equivalent and Figma-safe. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-pronged fix for the Figma loader Syntax-Error crash:
1. main.js was being emitted as an IIFE — \`"use strict";(()=>{...})();\`.
Figma's plugin runtime hosts the bundle in QuickJS and prefers
plain top-level statements. Switched esbuild format from \`iife\` to
\`cjs\` so the output begins with \`"use strict"; var ... =\` instead.
No \`require()\` calls in the bundle (everything is inlined), so cjs
collapses to flat top-level statements that the host parses
straightforwardly.
2. manifest.json had \`ui: { default: ..., inspect: ... }\` — those
keys don't match Figma's schema. The schema uses \`figma\` and \`dev\`
for per-editor UI bundles. To rule out per-mode wiring issues
while triangulating, the manifest is temporarily reduced to the
minimum: api, id, name, main, ui (string), editorType: ["figma"],
networkAccess. We re-add documentAccess, capabilities, per-editor
UI, menu, and relaunchButtons once the loader is happy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Figma's plugin sandbox runs on a QuickJS host whose syntactical
surface is ES2017. Optional chaining (?.), nullish coalescing (??),
and logical assignment operators (??=, ||=, &&=) are ES2020+ and
surface as "Unexpected token {" in jsvm-cpp.wasm with no useful
location — the parser fails at the first such operator it sees,
which in our minified bundle often lands at an object literal's
brace.
esbuild target switched from es2020 to es2017 for the main bundle
only. The UI bundle keeps es2020 because its iframe runs in
Chrome/V8, not QuickJS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix stripped both \`type=\"module\"\` and \`crossorigin\` from the inline script tag, which loaded the UI as a classic script. That's wrong: Vite emits the bundle as an ES module (uses \`import.meta\` semantics, top-level module scope) so executing it as a classic script silently fails and leaves the iframe blank. The actual blocker was the \`crossorigin\` attribute alone — Figma's null-origin plugin iframe can't satisfy the cross-origin handshake. Inline \`<script type=\"module\">\` itself works fine, it just must not carry crossorigin. Both Vite plugins now use a single regex that drops crossorigin and preserves type=\"module\". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that the plugin successfully boots, re-add the features we temporarily removed while triangulating the syntax-error crash: - editorType: ["figma", "dev"] enables the dev-mode inspect panel - documentAccess: "dynamic-page" matches the async page-load code already in the main thread - capabilities: ["inspect"] gates the dev-mode UI - per-editor ui mapping points figma -> ui.html, dev -> ui-inspect.html - menu adds Open / Toggle Annotations / Refresh Annotations commands - relaunchButtons adds "Edit Tolgee key" on connected text nodes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Figma exposes the UI source as one of two globals:
- \`__html__\`: string — set when manifest.ui is a string
- \`__uiFiles__\`: object — set when manifest.ui is a per-editor object
We switched manifest.ui to the object form (\`{ figma, dev }\`) to get a
separate inspect-panel bundle, but main.ts still referenced \`__html__\`
which is undefined in that mode. Plugin crashed with
"ReferenceError: '__html__' is not defined" before figma.showUI ever ran.
main.ts now picks \`__uiFiles__.dev\` in Dev Mode and \`__uiFiles__.figma\`
in design mode, falling back to \`__html__\` for safety.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With \`documentAccess: \"dynamic-page\"\`, Figma refuses to register a \`documentchange\` handler unless \`figma.loadAllPagesAsync()\` was called first. That call is expensive on large files (it materializes every page in the document) — exactly the cost \`dynamic-page\` was designed to avoid. We don't need \`documentchange\` for annotation reconcile: the \`selectionchange\` reconciler catches user-visible drift, and we already kick the reconciler explicitly after our own \`set-nodes-data\` and \`apply-translations\` writes. Cross-plugin edits to tolgee_info are an edge case the user can address via the \"Refresh Annotations\" menu command. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auth store was only populated when the user clicked "Test Connection" in Settings. On a fresh plugin open, the merged config already contains apiUrl + apiKey (loaded from clientStorage by the main thread), but the auth store stayed cleared — so the UI showed the "Sign in" CTA until the user re-opened Settings and saved the already-correct values. App.svelte now runs maybeBootstrapAuth() on init / config-changed / page-changed: if apiUrl+apiKey are present, it silently calls validateApiKey and populates auth.setAuth on success. A fingerprint guard prevents re-validating the same credentials repeatedly. If the projectId we learn back differs from what's stored on the document, we send persist-project-id so the inspect panel's deep-links stay project-aware without user interaction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously \"Toggle Canvas Annotations\" and \"Refresh Annotations\" from the plugin menu invoked the plugin like any other command — figma opened the UI window, then the ui-ready handler did the toggle as a side effect. So clicking the menu item *both* toggled annotations *and* popped the UI, which is not what the user expects. main.ts now branches at startup: - If figma.command is one of the quick-action commands and we're in design mode, run the side effect and call figma.closePlugin(msg) with a notify message. No showUI / attachBus. - Otherwise, open the UI normally. The Dev Mode case still skips quick-actions because the dev-mode plugin is read-only — annotation writes are disallowed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Selection-based reconcile + currentpagechange full-sync + explicit reconcile after our own set-nodes-data / apply-translations writes cover ~95% of drift. The Refresh button was a fallback for the remaining edge cases (manual annotation delete on a never-selected node, parallel cross-plugin edits) that few users will hit. Removing it shrinks the menu surface and one less concept to learn. If we need it later: re-add to menu, restore the cmd branch in ui-ready, re-add the union member in shared/messages.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bundle-size optimisation in phase 10 dropped @tginternal/editor
to save ~520 kB raw. That removed the syntax-highlighted ICU editor
the old plugin shipped, leaving a plain <textarea> in StringDetails
and Connect. The user noticed the visual regression.
Strategy: keep the local 130-LOC tolgeeFormat parser for pushDiff /
pullDiff (still saves the React + CodeMirror tax there), but mount
CodeMirror 6 only where the editor actually renders.
- Reinstated runtime deps: @codemirror/{state,view,commands,lint}
and @tginternal/editor 1.16.1. The React Editor.tsx wrapper from
the package is unusable from Svelte, but the underlying CodeMirror
extensions (tolgeeSyntax, TolgeeHighlight, PlaceholderPlugin) are
framework-agnostic.
- New IcuEditor.svelte: hand-rolled CodeMirror 6 mount with history,
drawSelection, default + history keymap, EditorState.readOnly when
disabled, line wrapping, and an updateListener that fires onChange.
External $effect keeps the doc in sync with the bound value when
the prop changes from outside (e.g. route load) without
re-emitting.
- Placeholder pill styles defined in component-local CSS scoped via
:global() so we don't need @emotion/styled.
- StringDetails.svelte translation textarea swapped for IcuEditor.
Bundle grows from 67 kB to 188 kB gzip; the highlight is back.
svelte-check 851 files 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old plugin let users type a Tolgee key directly into each unconnected text row in the index, with a debounced auto-save. The rewrite had collapsed that into a card that only navigated to the Connect view, which felt heavy for the common case of just labelling a few texts. NodeList now delegates to a NodeListItem component per row: - Connected: read-only display of ns.key, click navigates to StringDetails for plural/translation edits. - Unconnected: inline <input> for the key, 300ms debounced set-nodes-data write, with a link icon button that opens the full Connect view (for key search, plural toggle, namespace pickers). Hydration is gated by node.id so user keystrokes don't get clobbered when the parent re-renders. keyFormat / prefillKeyFormat from the saved config seeds the input on first hydrate for unconnected nodes. svelte-check 852 files 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The push / pull / createCopy / projectMeta API calls all forwarded
\`branch: cfg.branch || undefined\`. On projects without branching
that produced
\`{ code: \"feature_not_enabled_for_project\", params: [\"BRANCHING\"] }\`
400s on the translations endpoint and broke push.
- Auth store now also carries branchingEnabled / namespacesEnabled.
- App.svelte calls getProjectMeta() right after a successful
validateApiKey and populates those flags. Fetch errors are
treated as "both off" (safe default that prevents 400s).
- Push / Pull / CreateCopy derive \`branch\` as
\`auth.value.branchingEnabled ? cfg.branch : undefined\` so the
parameter only goes on the wire when the project actually
supports it.
While here:
- screenshots.ts and projectMeta.ts no longer poke at openapi-fetch
internals. They take \`apiUrl\` + \`apiKey\` directly. The auth
store now caches those strings alongside the typed client. Fixes
the "Could not determine baseUrl for screenshot upload" error.
- auth.setAuth signature gained apiUrl + apiKey; App.svelte and
SettingsSectionConnection.svelte updated accordingly.
svelte-check 852 files 0 errors, 87 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The IcuEditor css selectors used .cm-placeholder-* — that's wrong.
Tolgee's PlaceholderPlugin sets className to plain
\`placeholder-widget placeholder-\${type}\` with no \`cm-\` prefix. The
placeholders rendered as bare text, so the editor looked like the
old plain textarea even though parsing was working.
Switched selectors to .placeholder-widget / .placeholder-variable /
.placeholder-tag / .placeholder-variant / .placeholder-hash /
.placeholder-error and added pill styling for each. hash uses indigo,
error uses Figma's danger tokens, others retain their previous colors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues with the IcuEditor highlight:
1. Plural keywords (\`plural\`, \`one\`, \`other\`, \`=0\`, ...) and the hash
placeholder \`#\` are styled by TolgeeHighlight via tag→color but
the colors were CSS variables (\`var(--color-text)\`, \`var(--icu-fn)\`)
that aren't reliably resolved inside CodeMirror's inline color
styles. Switched to concrete hex colors (green for keywords, blue
for variableName) and \`currentColor\` for plain text so it still
inherits Figma's theme.
2. Placeholder widget tag classes are \`placeholder-tagOpen\` /
\`placeholder-tagClose\` / \`placeholder-tagSelfClosed\` — not
\`placeholder-tag\`. There is no \`placeholder-variant\` either;
plural variant descriptors are styled via the syntax-highlight
tag, not as widgets. Fixed selectors to match what the plugin
actually emits, plus added the variable color in concrete hex.
After this, in \`{count, plural, one {1 item} other {# items}}\` you
get: \`count\` and \`#\` highlighted as widgets / colors, \`plural\` and
\`one\` / \`other\` in keyword-green, content in default color.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues surfaced in StringDetails: 1. Clicking Save threw \`DataCloneError: Failed to execute postMessage on Window\`. The set-nodes-data payload carries paramsValues, which is a Svelte 5 \`\$state\` reactive proxy. The structured-clone algorithm browsers use for postMessage doesn't know how to serialise that proxy and bails. Fix lives in ui/lib/bus.ts: every outbound message is JSON-cloned right before parent.postMessage, which strips all Svelte proxies into plain objects. Safe because our UI→Main payloads are JSON-only (screenshots flow the other way as Uint8Array). All other call sites — Settings save, Connect, Push, CreateCopy — benefit from this without code changes. Also added an explicit \$state.snapshot in StringDetails.save() as a belt-and-braces measure / documentation. 2. The translation textarea showed empty on open even when the node already had a translation. Root cause: IcuEditor's \`view\` was a plain \`let\`, not reactive, so the sync \$effect that mirrors external value changes into the CodeMirror doc only ran with view===null on first pass and never re-ran after onMount assigned the EditorView. Switching to \`\$state<EditorView | null>(null)\` makes the effect re-fire once the editor mounts, which then correctly dispatches the buffered value into the doc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related problems:
1. Save did not change the Figma text. set-nodes-data only writes
plugin-data — \`TextNode.characters\` stays whatever the layer had
before. The pull flow already had \`apply-translations\` which loads
fonts and rewrites characters in addition to plugin-data; we now
route StringDetails save through that same path.
- shared/messages.ts: \`apply-translations\` updates can now carry
pluralParamValue, paramsValues, key, ns, connected so a single
round-trip persists the full StringDetails payload alongside the
rendered text.
- main/nodes/selection.ts applyTranslations: writes every supplied
field into NodeInfo plugin-data after writeTextNode.
- main.ts apply-translations handler: re-emits selection after the
write so the UI sees the fresh NodeInfo (including translation,
new characters) without an extra fetch.
- StringDetails.save: renders the ICU translation via
formatIcuMessage(translation, paramsValues, language) and sends
apply-translations with both text (rendered) and translation
(raw ICU source).
2. IcuEditor textarea showed empty when the node already had a
translation. \`EditorView\` is a class instance — wrapping it in a
\`\$state\` proxy is unsafe (proxy breaks CodeMirror's internal
invariants). Switched to \`\$state.raw<EditorView>\`: still tracked
so the sync effect re-runs once view is constructed, but no proxy
layer. Also rewrote the sync effect to read both reactive deps
(value + view) up-front so the early-return path doesn't drop
value from the dependency set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a TextNode carries both a Tolgee annotation and a manually-added one from another source, toggling annotations off threw a Figma error and stopped the whole sync. Two defenses: 1. \`normalizeAnnotation\` rebuilds each preserved entry as a fresh plain object containing only the documented Annotation fields (label, labelMarkdown, properties, categoryId). The read-only Annotation objects Figma returns from \`node.annotations\` may carry internal markers that the setter can't round-trip; round-tripping through a clean object avoids that. 2. \`writeAnnotations\` wraps the \`node.annotations = ...\` setter in try/catch, logs a warning, and continues iterating instead of aborting the whole sync. A single misbehaving node no longer blocks the toggle from clearing the rest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix used a normalize step that rebuilt every preserved Annotation entry as a fresh plain object. That looked safe but caused silent reverts on nodes with foreign manual annotations: the Figma setter accepted the assignment without throwing, yet the actual array on disk stayed unchanged — likely because the clone dropped internal markers Figma uses to recognise an existing annotation, so the setter treated everything as a new insert and ignored the operation. Switched to assigning the read-only Annotation objects back verbatim. Try/catch around the setter still protects us against genuine throws. A manual annotation alongside our Tolgee tag now survives the toggle and the Tolgee tag is actually removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues showed up after a successful push:
1. The overlay rectangles Tolgee draws on uploaded screenshots
appeared in the wrong place. We were exporting frames at scale 2
while sending node positions in 1x design-space coordinates — so
every box landed at half-size and shifted from the visible text.
The legacy plugin exports at 1x for this reason. Reverted
EXPORT_SCALE to 1.
2. The Push view re-listed already-pushed plural keys as "changed"
even though nothing had changed. Tolgee re-emits plurals in a
canonicalised form that extracts a common suffix out of every
variant, e.g. our pushed
\`{n, plural, one {One dog is here.} other {# dogs are here.}}\`
comes back as
\`{n, plural, one {One dog is} other {# dogs are}} here.\`
These render identically for every plural case but the strings
(and the parsed TolgeeFormat trees) differ — our isChanged check
correctly saw the mismatch and reported a change.
Added pluralRendersAreEqual: when both forms claim plural, we
format the ICU on both sides via intl-messageformat against a
small set of probe values (0/1/2/5/11/100) and consider them
equal when every output matches. Only when the rendered outputs
actually differ do we still flag a change.
extractParam pulls the plural parameter name from either source
so the probe-render uses the right placeholder; falls back to
"count" when neither side declares one. Wrapped in try/catch so
malformed ICU doesn't crash the diff.
87 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The diff between Figma and Tolgee compared \`textOfNode(node)\` — which falls back to \`node.characters\` when \`translation\` is empty — against the raw text Tolgee returns. On the first push of a connected node we never wrote the pushed string into plugin-data, only marked the node connected. So the next push diff compared the raw rendered characters against Tolgee's canonicalised plural form (suffix extracted out of every variant) and reported every plural key as "changed" forever. After a successful push we now persist \`translation: n.translation || n.characters\` alongside \`connected: true\`. Subsequent diffs run local vs. remote with both strings rooted in the same source, and pluralRendersAreEqual catches the remaining case where Tolgee's canonicalisation produces an equivalent-but-different ICU shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Tolgee annotation category already gives these entries a distinct identity in the annotation panel — wrapping the key in \`**...**\` was visual noise. Plain text reads cleaner and matches how other plugin annotations typically render. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix stored the locally-pushed translation string into plugin-data. That still produced spurious "changed" diffs whenever Tolgee rewrote the ICU we sent — e.g. extracting a shared suffix out of every plural variant — because our cached source no longer matched what the server now holds. Now: after a successful push, refetch the same keys from Tolgee (/v2/projects/translations filtered by the keys we just pushed) and store the server's canonical translation as the node's translation property. The next diff then compares like for like; no more phantom plural diffs. Best-effort: if the refetch fails, fall back to the locally-pushed text so connected nodes never end up with an empty translation field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Even with identical translation strings, the diff still reported
"changed" when the local node's \`isPlural\` flag was false while
Tolgee inferred \`keyIsPlural: true\` from the ICU shape. The
isChanged check has a dedicated post-string-compare branch that
fires whenever those flags differ.
Now fetchCanonicalAfterPush returns \`{ translation, isPlural }\` and
we write both into plugin-data after a successful push, so the next
diff has a consistent plural-flag on both sides. Falls back to the
local value when no remote entry is available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Header component had props for languages / namespaces / branches but nothing fed them. The language Select only showed the currently saved value (\`de\`) with no other options to switch to. App.svelte now fetches /v2/projects/languages and /v2/projects/used-namespaces right after a successful auth bootstrap and stores the lists on the auth store. Errors are swallowed (the header falls back to "current value only" when the lists are empty). Index.svelte maps those lists into Select option shape and forwards them to Header along with branchingEnabled. Selecting a new language fires set-language as before; the main thread persists it and emits config-changed which the rest of the UI reacts to. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the header language Select fired set-language directly, which persisted the new language and emitted config-changed — but nothing routed the user to a pull view, and the Pull route stayed stuck on its initially-bootstrapped language until it was remounted. New flow matches what you asked for: - Header.handleLanguageChange navigates to Pull with the new language in the route. No set-language is sent. The saved language is untouched. - Pull.applyChanges does the persist: when language differs from the saved one, it sends set-language right before triggering the apply-translations round-trip. So picking the language and clicking Apply commits both the language and the translation writes together. - Cancelling Pull (goBack) leaves the saved language intact — the header Select snaps back to the persisted value the next time the view repaints. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull always sent \`filterNamespace: [namespace]\` where \`namespace\` fell back to the empty string when the document hadn't configured one. On the Tolgee API, \`filterNamespace: [""]\` filters to the default namespace only — every key stored under a real namespace got dropped. So after switching language the diff was always "no changes" simply because we never fetched the relevant keys. - fetchAllTranslations now accepts \`namespaces\` as optional. Empty array or omitted → a single paginated pass with no \`filterNamespace\` query param, so the server returns every key in every namespace. - Pull.svelte forwards \`undefined\` when \`config.namespace\` is empty and a singleton array otherwise. CreateCopy still passes its own list explicitly (uses the pinned namespace + the default fallback). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy plugin classified a connected node with no translation in the requested language as "missing" — that signal nudges the user to go translate the key in Tolgee. Our diff was bucketing it as "unchanged", so switching to a language that has gaps showed "no changes" and left the user thinking nothing needed pulling. Two changes in pullDiff: 1. Empty / falsy remoteText (key exists but no string for this language yet) → missingKeys, mirroring the legacy behaviour. We still don't push an empty string onto the canvas. 2. New legacy-equivalent branch: when the cached translation matches the remote but the rendered \`characters\` have drifted (e.g. someone retyped the layer manually) AND the key is non-plural without params, classify as changed so we can re-apply the source of truth. Test "treats empty remote as unchanged" updated to "classifies as missing", reflecting the new contract. All 87 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…opping them eweren's Medium #8. A frame whose export threw was caught, `console.warn`'d (dev-only, invisible to users) and skipped. `captureScreenshots` returned only the delivered count, and the push summary printed that as though it were the whole set — "N screenshot(s) uploaded", with nothing to say M others never made it. A designer would have to notice the missing screenshots in Tolgee to find out. Count the failures alongside the deliveries, carry them on `screenshots-done`, and say so in the summary: "N screenshot(s) uploaded, M failed to capture." The counter resets at the start of each push — screenshots are optional, so a run with the toggle off would otherwise inherit the previous run's failures in its summary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…blocked Two of eweren's Medium findings in the copy flows. #10 — Recreate was re-entrant across the unmount it is designed to survive. `finishCopyRecreate` deliberately outlives the view that started it, because recreating deletes the current page and unmounts CopyView. The "busy" flag did NOT: it lived in component state, so a second click landed on a fresh instance whose own `stage` read "idle". Since recreating DELETES the existing copy page, that second run would remove the page the first was midway through writing. Track the in-flight job beside the other module-scoped state that already survives the unmount, and check it in `recreateCopy` BEFORE sending `create-copy` — by the time the continuation registers, the main thread is already deleting. An older job settling can't clear a newer one's mark. The fire-and-forget `void (async … )()` also gains a `.catch`: a throw in the apply chain surfaced only as an unhandled rejection, leaving the view stuck on "recreating" with no message. #11 — `hasMissingFont` skipped a layer with nothing to show for it. Such a layer keeps its ORIGINAL translated text instead of the key label, so a keys copy came out looking like the plugin had quietly failed on exactly those layers. `writeTextSafely` now reports whether it wrote, and the names are collected into `CreateCopyResult` and counted in the completion notice: "Created keys page — N layer(s) skipped (missing fonts)." Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… list
Codex P2 on `pickers.ts:79`. `openapi-fetch` RESOLVES on 4xx/5xx with an
`error` field instead of throwing, and `fetchBranches` destructured only
`data` — so a rejected or failed request returned `[]` on the SUCCESS path.
`hydrateBranches` then set `branchesLoaded: true`, and with a branch
configured `isConfiguredBranchMissing` announced that it had been deleted in
Tolgee, offering an empty picker to replace it. Its own `catch` was there to
prevent exactly this ("a failed fetch says nothing about which branches
exist") — it just never fired, because nothing threw.
Throw on `error`, so the failure reaches the `loaded: false` branch that was
already written for it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rs panel Codex P2 on `scan.ts:199`. With "ignore hidden layers including children" enabled, the recursive selection scan started every selected root at `ancestorHidden = false`. The walk only ever looks downward, so nothing ABOVE the selected root was examined: picking a visible text node or nested frame from the Layers panel while one of its parents was hidden let that text show up and be synchronised, while a page-wide scan correctly excluded it via `computeAncestorHidden` — the same helper, just never applied here. Seed the traversal with the root's own ancestor state. `computeAncestorHidden` already walks up to the page and is widened from `TextNode` to `SceneNode`, which is all it ever needed (it only reads `parent`). Verified the test fails without the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex P2 on `clipboard.ts:80`. When `execCommand` didn't fire the copy handler but `navigator.clipboard` existed, the fallback ran `void clip.writeText(text)` and returned `true` immediately. `writeText` rejects whenever the document lacks focus or clipboard permission, so the caller flashed its confirmation while the clipboard still held its old contents — the same "looks like it worked" failure the rest of this module exists to avoid. Await it. The function becomes `async`, which does NOT weaken the "call synchronously from a user gesture" contract it documents: everything up to the first `await` — the whole `execCommand` path — still runs inside the gesture's own tick, and the only await is in the fallback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…literal
Codex P2 on `keyFormat.ts:141`. An empty placeholder dropped the entire
adjacent literal PART, so any fixed text in it went with the separator:
`foo{component}bar{elementName}` with no component produced `fooName`, losing
`bar`. These values are persisted as Tolgee keys and uploaded, so the loss is
silent and durable.
Trim one separator CHARACTER instead. "Separator" is anything that isn't a
letter or a digit — `\w` would keep `_`, which is the separator most templates
actually use.
Worth recording that all three candidate behaviours differ here. The published
plugin (`preformatKey.ts:30`) removes everything between the previous `}` and
the placeholder, giving `barName` — and on `app.{component}.{elementName}` it
eats the user's `app.` prefix entirely. This rewrite already diverged from
that deliberately (its doc comment cites `{component}.{elementName}` →
`element_name`, not `.element_name`), and the change keeps every documented
case identical: all 25 existing tests pass untouched. It differs from the old
behaviour ONLY where a literal carries text beyond its separator, which is the
case being fixed.
Migration impact is limited by `prefillKey`: a key already assigned is never
regenerated by a format change, so this affects newly generated keys only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lide
Codex P2 on `CreateCopy.svelte:102`. Composite `(namespace, key)` map indexes
were built as `${ns}|${key}`, but `|` is legal in both a namespace and a key
name and neither input forbids it — so `(ns "a|b", key "c")` and
`(ns "a", key "b|c")` both spell `a|b|c`. The later entry overwrote the
earlier, and in the copy flow `buildCopyUpdates` then wrote that one
translation onto BOTH cloned nodes.
The report named one site; the pattern was in seven — `pushFlow`'s
`canonicalKey`/`resolutionKey`, `pullDiff`'s remote index and its lookup,
`pushDiff`'s conflict grouping, `copyApply`'s translation lookup, and the two
copy views that BUILD the map `copyApply` reads. Those last two are why this
had to be done in one go rather than at the reported line: the index is
constructed in one file and consumed in another, so changing either alone
silently finds nothing.
All of them now go through `nsKeyIndex`, which joins on NUL — impossible in a
Tolgee key or namespace, and the separator the published plugin uses for its
own composite keys. Tests assert through the helper rather than a literal, so
the separator can change again without a sweep.
Also carries a fixup to 027c8f2: `dispatchCreate`'s return type never declared
`skippedMissingFont`, a type error that shipped because NOTHING in the gates
checks `.svelte` files — `tsc --noEmit` skips them and biome doesn't process
them at all. Found by running the installed-but-unwired `svelte-check`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing in the gates looked at `.svelte` files. `tsc --noEmit` skips them,
biome doesn't process them ("No files were processed"), and components have no
unit tests — so for a PR that is largely a Svelte rewrite, the largest part of
the diff was verified by nothing. `svelte-check` was already a dependency; it
was simply never called.
Found the hard way: an `await` written into a non-async arrow — a syntax error
— passed all three gates as green, and a type error in `CreateCopy.svelte`
reached a commit (fixed in 86b4275).
Wires it in as `typecheck:svelte` plus a CI step, and clears the 7 pre-existing
errors that were keeping it red:
- `Index.svelte` — unused `TolgeeConfig` import.
- `Index.svelte` — `checkedKeys` declared its own `{ name; ns? }` shape while
`fetchMissingKeys` takes `ConnectedKey`, whose `ns` is required-but-
nullable. Same values at runtime (`connectedKeySig` normalises undefined
and "" to one signature); the two declarations just never agreed. Now uses
the imported type.
- `Index.svelte` — `placeholderData` typed its argument `ReadonlySet` where
the query yields `Set`.
- `Connect.svelte` / `StringDetails.svelte` — `sel[0]` is `T | undefined`
under `noUncheckedIndexedAccess` even behind `sel.length === 1`. Added a
fallback that cannot run, rather than an assertion.
- `CopyView.svelte` / `CreateCopy.svelte` — `aria-label` on `Select`, which
didn't declare it. Kept the label and taught the component to forward it:
a disabled single-option select has no visible label, so that attribute is
its only accessible name.
None of the seven was hiding a bug — they are type-hygiene, and the `ns` one
(the only candidate) turned out to be two declarations of the same runtime
shape.
Also collapses a duplication this branch introduced: `connectedKeySig` was
already joining `(ns, name)` on NUL for exactly the same purpose as the
`nsKeyIndex` added in 86b4275. It now delegates, so the two can't drift.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…being overwritten I had this backwards and want the reasoning recorded rather than repeated. Chasing "a user migrating from the published plugin can end up with nodes whose cached translation matches the remote while the canvas is stale, and nothing ever rescues them", the obvious fix looked like relaxing the plural/params exclusion in `pullDiff`'s drift branch — `canvasDrifted` renders with the node's OWN sample, so it seemed safe. It isn't. Drift is measured against that sample, and a node migrated from the published plugin has no usable one: that plugin stored the argument NAME in `pluralParamValue` (its own bug, already noted in interpolate.ts), which `numericCount` rejects, so the sample falls back to 1. Measured directly: a migrated layer reading "10 days" renders as "1 day". Relaxing the exclusion would flag every such layer as drifted and rewrite the design to the wrong plural form — on every pull, silently. The exclusion is the thing preventing that, not caution to be tidied away. So: no behaviour change. The comment now says WHY the exclusion exists (it previously only said re-applying was "safe" for the other cases, which reads as an omission rather than a decision), and three tests pin it — a migrated plural layer and a parametric one must stay untouched, a genuinely drifted plain layer must still be re-applied. Verified the first fails if the exclusion is removed. The underlying gap is real but not fixable here: the sample data is wrong, not the diff. Repairing it from the canvas (`inferPluralCount` already does exactly this at connect time) would fix the cause without touching the design — worth doing deliberately, not as a side effect of a diff tweak. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found while fixing the plural seeding in ff1d7bb, not reported by either reviewer: `namedPlaceholders` matched arguments with `\{\s*(\w+)\s*(?:,[^{}]*)?\}`, and `[^{}]*` cannot span a nested brace. Any argument whose body holds braces — every `select`, and a plural wrapped in one — was therefore never seen and never seeded, so `IntlMessageFormat` threw MISSING_VALUE on it: the node was dropped in `copyApply` and Pull left stale text on the canvas. Same failure as the plural fix, one argument type over. Replaced with `findArgumentNames`, a brace-aware scan sharing the walker that `findPluralParameters` already used. That walker needed one thing it didn't have. Inside a plural or select, the immediate `{…}` runs are variant BODIES, not arguments — in `{gender, select, other {x}}`, `{x}` is the body of `other`, and collecting it would invent a parameter named `x`. It now tracks whether it is inside a variant list, so bodies are descended into but not reported, and `one {Hello {name}}` still yields `name`. My own new tests caught this; the first version reported `x` and `hi` as arguments. The interpolate test that documented this gap by working around it (asserting on params because the full render couldn't succeed) now asserts the render. Measured, since this puts a walk where a regex was, on a per-node path: plain text is FASTER (0.09 vs 0.16 µs — it bails on the first scan with no braces), a typical parameterised string is identical, a plural is 3x slower at 0.47 µs, and the worst realistic case is 4.6 µs. Beside the ICU render on the same path that is noise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last of the review's flagged test gaps, and the PR description's own
headline trade-off: the published plugin raised a conflict on ANY plural-form
difference, which drowned the legitimate case (each layer pluralised from its
own sample carries only the form its count selects). Suppressing that was a
product decision — but it means a REAL disagreement inside one category now
resolves first-wins, silently, and nothing pinned that down. Only
non-overlapping categories were covered.
Four tests: the two layers don't conflict; the first layer's body wins for
every shared category and the second's text is dropped (asserted, because it
is accepted behaviour rather than an oversight); non-shared categories are
still unioned, which is what the merge exists for; and the regenerated ICU
still parses with a nested argument intact.
That last one confirms the reviewer's hunch that a test here would have caught
the escaping bug: reverted to the pre-7bc393e `escapeIcuBraces` and it fails,
because `mergePluralForms` regenerates the whole string through exactly that
path and `{name}` came back as literal text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…list loads Takes the real half of eweren's Medium #9 and declines the other half, with the reasoning recorded so the disagreement doesn't have to be relitigated. The real half: `LANG_TAG_RE` accepted any 2-3 lowercase letters, so renaming "Home - cs" to "Home - wip" was read as a language — silently overriding the immutable `copyLanguage` marker and breaking Download for that page. Now the suffix must be a KNOWN project tag whenever that list has loaded. The shape check remains for the empty-list case, which is not a detail: on a copy page the language list is usually still in flight when Recreate/Download runs. The review's suggested fix — constrain the pattern to the project's actual tags, full stop — would reject a legitimate "cs" there and fall back to the main language, which is precisely the bug c3a09f6 fixed. The other half was marker-first precedence, and the name must keep winning: - A copy made by the PUBLISHED plugin has no marker to prefer. `src/main/endpoints/copyPage.ts:27` stores `{pageCopy, pageInfo, language}` and nothing else, and that `language` is the repointable Push/Pull one — the very value this resolution exists to distrust. Marker-first would send those copies straight to it. The name (`copyPage.ts:17`) is the only stable signal such a copy leaves. - Name-first is self-healing. Copies whose marker was poisoned to the main language before the resolution was fixed recover as soon as the name is read; marker-first would keep them broken indefinitely. `CopyView.svelte`'s comment claimed marker-first while the code did the opposite — the contradiction the review reasonably picked up on. It now describes the actual order and why. Tests cover both halves, including the two cases that decide the precedence: a poisoned marker losing to a correct name, and a published-plugin copy with no marker at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d024638 was staged with `git add -A` instead of scoping it to `src/`, so three
untracked things rode along that have nothing to do with this branch:
- `delivery/tolgee-plugin/killer-decathon.png` (743 kB) — a stray image
- `.claude/{settings,launch}.json` — local Claude Code config, machine paths
and all; now gitignored so it can't happen again
- `pnpm-workspace.yaml` — untracked here before this branch; whether it
should be committed is a separate decision, not one to make by accident
Untracked, not deleted: the files stay on disk exactly as they were.
This removes them from the branch TIP only. The blobs remain reachable in
d024638 itself, so the 743 kB is still in history — stripping that needs a
rewrite of the range, which is worth doing while the branch is unpushed but is
not something to do unannounced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sweep in 86b4275 missed three sites, and the reason is worth recording: `nsKeyIndex` was put in `$ui/lib/logic`, which `src/main` cannot import from. So the main thread kept its own `${ns}|${key}` join and nothing pointed at it. Moved to `$shared/keyIndex`, where both threads can reach it, and the stragglers converted: - `createCopy.ts` — the per-key counts behind copy staleness. The worst of the three: staleness compares COUNTS, so a collision doesn't mislabel a key, it cancels a real difference out. Source `(ns "a|b", key "c")` and copy `(ns "a", key "b|c")` both spelled `a|b|c`, both sides counted 1, and the copy read as up to date while having gained AND lost a string. - `Index.svelte` — the `{#each}` key of the unconnected-match list. - Stale comments in `createCopy.ts` and `CreateCopy.svelte` still describing the old format. Tests drive the collision through `checkCopyStaleness` and assert both counts; verified they fail against the `|` join. Found by auditing for leftover joins rather than by a test — nothing had covered the main-thread path, which is how it survived the first pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A key changed only by a missing tag, or by the plural flag, rendered as the usual strike-through diff — with the same string on both sides. There was no way to tell that from a broken diff. That is not a hypothetical readability nit. It is what produced eweren's "push always shows changes" report, and it cost a round of manual testing yesterday to establish that a screenshot of three identical-looking rows was CORRECT behaviour (tag sync on, tag not yet on those keys) rather than the bug it looked like. `isChanged` becomes `changeReason`, returning "text" | "plural" | "tags" or null, and `ChangedKey` carries it. Text wins when several apply — the strike-through shows real content, where a tag note would hide it. The row keeps the diff for a text change and replaces it otherwise with what actually happens on push: "Text unchanged — the configured tag will be added." Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`delivery/tolgee-plugin/` was still the Jul 24 build while the PR description states it mirrors `dist/` byte-for-byte — so the claim had been false for the whole review cleanup. Now md5-identical to `dist/` again, and the zip is rebuilt from it. The stray `killer-decathon.png` sitting in that folder is explicitly excluded from the archive; it was never part of the package and only ended up nearby. `manifest.json` is unchanged, and `APP_VERSION` stays 2.0.0: the policy in 7affe32 is to bump on new functionality, but 2.0.0 IS this PR and has not shipped, so there is no released version to move away from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The specs look elements up by their visible label, and this branch renamed Push → Upload and Pull → Download in the UI (a deliberate call: the users are designers, so only the labels moved — routes, files and test names stay push/pull). The specs never followed, so they were searching for buttons that no longer exist and timing out. 55 locators across 6 spec files, plus one comment quoting the old heading. Nothing about the plugin was wrong; the specs were out of date. This does NOT make the suite green. A second layer of drift shows up behind it — `connect.spec.ts` waits for "1 selected" where the Index now reads "1 string" — and there are likely more, since each round surfaces another. Left for a separate pass rather than widened into the review cleanup. Worth fixing the cause first: `gh pr checks` reports no checks have EVER run on this branch, which is why months of label changes could drift unnoticed. The workflow does define an `e2e` job; it has simply never executed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hare
Second pass over the drift. `branches.spec.ts` is fully green again (9/9 in
5.6s, down from 5 failures and 2.5 minutes of timeouts); the shared label fixes
move the other specs forward without finishing them.
What was actually wrong in branches.spec.ts — none of it a plugin bug:
- The mocks were `{ times: 1 }`, but the plugin POLLS the branch list. Calls
two and three fell through to the real API, which 400s on a project with
branching disabled, and the empty result wiped the list the first call had
populated. Both intercepts are unbounded now, and documented as such.
- A test asserted a namespace select in the header. That was removed in
ba90c02 — the picker lives on the rows and in the bulk action now — so the
assertion was checking UI that no longer exists. Dropped, with a note.
- Unbounded intercepts outlive the assertions, so a poll in flight at
teardown made the handler throw on a closed page. Both handlers now
tolerate it.
Shared drift fixed across specs: `1 selected` → `1 string` (61), the connect
row action `getByTitle("Connect to existing key")` →
`getByRole("button", { name: "Connect to key" })` (15, it is an aria-label now,
not a title), and the Connect view heading (9).
`connect.spec.ts` is NOT fixed and needs rewriting rather than repair: it
drives a key input, a Connect button, a Cancel button and a plural toggle, and
that view is search-driven now — none of those exist. Same likely for others.
Left alone rather than guessed at.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…reen
Second half of the mechanical repair. 62 passing, up from 55 this morning and
from near-zero before any of it. `pull`, `push`, `page-setup` join `branches`,
`copy-view` and `dev-mode` at zero failures.
Two of these were not label drift but a semantic change the fixtures had not
followed, and they are worth naming:
- `push`: two tests built a "new key" node as `connected: true`. A CONNECTED
node whose key is absent remotely means the key was DELETED on the
platform, and `pushDiff` deliberately withholds those rather than silently
re-creating what someone removed — so the diff had nothing to push and the
button was right to stay disabled. Fixtures now say `connected: false`,
which is what "a new local key" actually looks like.
- `push`: "disabled when no changes" ignored `screenshotOnlyUpload` — with
the screenshot toggle on and unchanged keys present the button stays
enabled BY DESIGN, so the test now turns the toggle off to reach the state
it means to assert.
Also: `getByText("Unsaved changes")` hit a strict-mode violation (the dialog
heading and its body both match) — the dialog was showing all along; the
locator was just ambiguous. And StringDetails' key is a textbox VALUE now, not
free text.
Still failing, all needing a REWRITE rather than a fix, because the views were
redesigned: `namespaces` (14), `settings` (4, tabs restructured from four to
three), `connect` (3), `index` (2, the page-wide count when nothing is
selected no longer exists), `create-copy` (2), `onboarding` (2),
`string-details` (2, plural is locked in that fixture now).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…exists 14 of its 15 tests drove surfaces that are gone: a namespace <select> in the header (removed in ba90c02 — the picker moved onto the rows and the bulk "Set namespace" action) and `#connect-ns` / `#connect-key` in a Connect view that is search-driven now. Repairing locators could not help; the design changed. Replaced with 7 tests covering where a namespace is actually visible today: - Settings Project tab offers a default-namespace FIELD (a free-text input with autocomplete, not a dropdown) when namespaces are enabled, and none when they are disabled. - Connect search results carry `ns:<name>`, spell out `ns:<none>` for the default namespace, and drop the badge entirely when the feature is off. - An unconnected Index row exposes the per-row namespace input — and does not when the feature is off. The project-meta intercept is unbounded, like the branches one: the plugin re-reads meta, so `{ times: 1 }` let later calls fall through to the real (enabled) value and the "disabled" tests never reached the state they name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The old file drove a "Test Connection" button, a top-right "Close" button and four tabs including "Connection" and "Sync". None exist: Settings has THREE tabs (Project / Strings and Keys / Upload options), a back arrow in the header, and Cancel + Save in the footer. Credentials are validated by SAVING now, not by a separate probe, so the two connection tests assert the outcome instead — landing on Index connected, or staying "Not connected" for a bad key. Two markers needed care: the "Key format" field only renders once "Prefill key name" is on, so it is the wrong thing to assert a tab switch with. The tab test uses the unconditional toggle label, and the save test turns that toggle on — which is both a real change to persist and what reveals the field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wizard has no "Set up Tolgee" heading and no "1/3" counter — it opens on a "Connection" step with a numbered stepper mirroring the Settings tabs. The walk to the end no longer counts Next clicks either: there are four steps now, and where the wizard starts depends on how much of the config already validated, so it advances until Save is offered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- index: the page-wide 'N on this page' count and its empty state are gone — with no selection the Index asks for one, the same scoping change Download got. Asserts that instead. - create-copy: the mode radios are `sr-only`, so Playwright cannot click them; clicks the visible label inside the <label>. - string-details: Plural is LOCKED for a connected key, so the toggle test now opens an unconnected node; the control is a CheckboxField <button>, not `#string-details-plural` (which never existed), and the dialog's Save is scoped to the dialog so it stops colliding with the footer's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The old file drove a free-text key field, a namespace field, a plural toggle and Connect/Cancel submit buttons. The view searches Tolgee and connects to a RESULT now — none of those existed, so re-pointing locators could not help. Seven tests over what the screen actually does: it opens with the search box PREFILLED from the node's own text (so it lands mid-search, not on the empty prompt), a no-match search says so, results carry per-row Connect, connecting navigates back with the node linked, an already-connected key offers Disconnect instead, and the back arrow returns to Index. A connected row has no "Connect to key" action, so the helper reaches the view through the overflow menu's "Connection detail" for that case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
|
Important Review skippedToo many files! This PR contains 442 files, which is 342 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (5)
📒 Files selected for processing (442)
You can disable this status message by setting the Comment |
v2 replaces the Preact codebase entirely, so every conflict was the same shape: a file this branch deleted and main went on to edit. All of them are resolved by keeping the deletion. The fixes main made in the meantime are not lost — each already has a native implementation here: - #103 (derive {component} from instances) → nodes/nodeParents.ts, which resolves the main component of an instance and adds {instance} as a variable of its own - #105 (key format cursor) → ui/lib/components/ui/keyFormatInput.svelte - #100/#98 (scan and font caching) → stores/app.svelte.ts, api/pageNodes.ts cypress/ and docs/superpowers/ are left out for the same reason: the rewrite dropped Cypress for Playwright, so a Cypress spec and the design notes for a Preact fix would document code that no longer exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collaborator
|
Closed my draft PR. Happy to see the figma plugins new version already being live 🙌 |
bdshadow
approved these changes
Sep 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This replaces the Preact plugin on
mainwith the v2 rewrite that is already published to the Figma Community (live since 8 September). Publishing it built the shipped package from26843a1, the head of this branch, somainis currently behind what users are running.It carries Nico's Svelte 5 rewrite (#101) plus the work that followed it: 254 commits, 53 from @eweren and 200 on top.
What changes
The UI is rebuilt in Svelte 5 with Tailwind and Vite, replacing Preact and react-query. Settings are reorganised into three tabs (Project, Strings & Keys, Upload options), and Push/Pull are renamed to Upload/Download throughout.
New: bulk operations — generate key names, auto-connect by exact match, connect to one key, set namespace, and disconnect, all across a whole selection.
Rebuilt: plurals and variables now render a sample on the canvas, page copies detect when they fall out of date and offer a rebuild, and the strings list gains search and filters.
Compatibility
Storage keys are unchanged —
tolgee_configinclientStorageand document plugin data,tolgee_infoper node — so existing key connections, the connected project and the API key all load without migration.writeGlobalSettingsdeliberately persists a JSON string rather than an object, because the published Preact reader does an unguardedJSON.parseat startup. That keeps a rollback tomainworking instead of throwing on every document.The manifest now sets
documentAccess: "dynamic-page", whichmaindid not.Testing
The e2e suite runs Playwright against a real Tolgee instance in Docker (
e2e/docker-compose.yml), which also covers the self-hosted path. It was green on this commit, and @eweren tested the build through before it was published.Note on #101
This branch contains #101 in full, so merging it makes that draft redundant. Closing it in favour of this one is the intent, but that is @eweren's call.
🤖 Generated with Claude Code