Skip to content

feat(plugins): let language packs translate the install form and log controls - #14121

Open
smwbev wants to merge 2 commits into
stablyai:mainfrom
smwbev:feat/translatable-plugin-chrome-wave2
Open

smwbev wants to merge 2 commits into
stablyai:mainfrom
smwbev:feat/translatable-plugin-chrome-wave2

Conversation

@smwbev

@smwbev smwbev commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Second narrow widening of the plugin-chrome exemption landed in #12514, following the same rule as the first: a path belongs here only if rewriting it cannot mislead the person deciding whether to trust a plugin.

This takes one of the two follow-ups you left explicitly open on #12455:

Same for PluginInstallDialog, which mixes a button label with consent copy.

Having read that module key by key, the mix is real but it separates cleanly. The dialog's form — tab labels, field labels, placeholders, the "you left this blank" validation — asserts nothing about any plugin. It collects a URL or a folder path before a plugin is even named. The one key in there that does argue a security position is gitRefRequired, and it stays protected.

The other follow-up — splitting PluginMarketplaceListingRow so installed and checkUpdate can move — is untouched. That module holds official and blocked in the same file, and that split is still your call, not mine.

What changed

23 exact paths, in three groups, appended to the allowlist in plugin-translatable-chrome.ts. Exact paths rather than a pattern, same as before: anything new stays protected until someone deliberately adds it.

Group Exempt Why it cannot mislead
PluginInstallDialog — the form 13 — gitTab, localTab, gitLabel, localLabel, both placeholders, gitUrlRequired, localRequired, source, title, install, installing, cancel Collects an address before any plugin identity exists; makes no claim about what will run
PluginSettingsRow — log affordances 9 — viewLogs, hideLogs, loadingLogs, noLogs, logCount, restarting, restartCount, running, moreActions Reports what an already-trusted plugin is doing after the decision, not before it
PluginsSettingsSection 1 — experimental Names how finished the feature is, not what a plugin may do

What deliberately stays protected

On the very same rows as the newly exempt copy:

  • Trust state on an installed row — blocked, bundled, dev, needsReview, invalid, viewAdvisory. These are what a reader weighs when deciding whether this plugin should run at all.
  • The actions that change that decision — enable, remove, rollback.
  • PluginInstallDialog.gitRefRequired — it argues why pinning a ref matters. That is a security claim, and it is the reason this module was flagged as mixed in the first place.
  • Everything the security tests pin whole, unchanged.

The boundary is deliberately awkward here: viewLogs becomes translatable while blocked two elements away does not. That asymmetry is the point — the rule follows what the copy asserts, not where it sits.

Screenshots

No visual change in English — no English string is added, removed, or reworded. Only the set of paths a language pack is permitted to override changes.

For context, Settings → Plugins in a build with a Russian language pack before this series began — the inner list translated while the frame stayed English:

Plugins pane

Experimental beside the heading and the Install plugin button that opens the dialog are both in this PR; the dialog's own form is what the 13 paths above cover.

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test
  • pnpm build
  • Added or updated high-quality tests that would catch regressions

plugin-language-pack-artifact.test.ts pins both directions: each newly exempt path parses, and the trust copy sitting beside it still fails with protected security copy. A future edit that widens the allowlist past the rule fails the second half.

src/shared/plugins/ is green end to end — 16 files, 153 tests. The full suite reports 75 failures in 9 files, all outside this diff: six IME/xterm specs under terminal-pane, plus ssh-posix-command-wrapper, agent-exec-handler and terminal-snapshot-osc8-roundtrip. Those fail on a clean checkout here too, and the count drifted between two consecutive runs (76 → 75), so they are environmental rather than deterministic.

AI Review Report

Reviewed the diff for correctness, security-boundary reasoning, and cross-platform impact on macOS, Linux, and Windows.

  • Verified every path still exists. The branch was rebased onto current main (83 commits of drift since it was written); all 57 allowlisted paths — 34 from fix(i18n): consolidate the open community translation PRs #12514 plus these 23 — resolve in en.json today. A stale entry would be dead weight in a security allowlist, so this was checked mechanically rather than by eye.
  • Checked the split against the rule, not the module. Each of the 23 was read in context to confirm it makes no claim about plugin behavior. gitRefRequired failed that test and stayed out, which is the same judgment that keeps systemDescription and the *Failed family protected in fix(i18n): consolidate the open community translation PRs #12514.
  • Cross-platform: the change is a data-only edit to a shared allowlist plus its tests. No paths, shell invocations, keyboard shortcuts, path separators, or Electron platform APIs are touched, and the parser it feeds is platform-independent.

Security Audit

  • Attack surface: this widens what a language pack may rewrite. Every added path was checked against the question the rule asks — can rewriting this mislead someone deciding whether to trust a plugin? Consent copy, trust badges, destructive confirmations and the whole *Failed family remain refused.
  • Fail-safe preserved: the allowlist is exact paths, not a prefix or pattern, so a new key added under any of these modules tomorrow is protected on the day it lands — the property that made the original broad rule correct.
  • Enforcement: the negative assertions in plugin-language-pack-artifact.test.ts mean the boundary is enforced by tests rather than by review memory.
  • Input handling / command execution / secrets / IPC / dependencies: untouched. No new dependencies, no runtime behavior change for packs that do not override these keys.

Notes

If you would rather take the log affordances and leave PluginInstallDialog for a separate pass, the three groups are independent and can be split — say the word and I will resubmit them apart.

…controls

Second narrow widening of the plugin-chrome exemption, following the same rule
as the first: a path belongs here only if rewriting it cannot mislead the person
deciding whether to trust a plugin.

Twenty-three paths, in three groups. The install dialog's own form — tab labels,
field labels, placeholders and the "you left this blank" validation — asserts
nothing about a plugin; it collects a URL or a folder path. The log affordances
on an installed row ("View logs", "Restarting", "No log lines recorded.") report
what the plugin is doing after the trust decision, not before it. And the
Experimental badge names how finished the feature is.

What stays protected is the reason the prefix is broad. Trust state on the same
row — `blocked`, `bundled`, `dev`, `needsReview`, `invalid`, `viewAdvisory` —
stays, as do enable, remove and rollback. `gitRefRequired` stays because it
argues why pinning a ref matters. The components the security tests pin whole
are untouched.

Tests pin both directions: the newly exempt paths parse, and the trust copy
beside them still fails with 'protected security copy'.
@smwbev

smwbev commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@nwparker — no reviewer again, same reason as #12455: CODEOWNERS covers /src/renderer/src/i18n/locales/ and /config/i18next.config.ts, but nothing under src/shared/plugins/, so this did not get auto-assigned.

This is the second wave you left room for, and it takes one of the two follow-ups you named on that PR:

Same for PluginInstallDialog, which mixes a button label with consent copy.

Reading it key by key, the mix separates: the form (tabs, labels, placeholders, blank-field validation) collects a URL or a folder path before any plugin is named, while gitRefRequired argues why pinning a ref matters. So twelve of thirteen move and gitRefRequired stays protected.

The other follow-up — splitting PluginMarketplaceListingRow so installed and checkUpdate can move — I left alone. official and blocked live in that same module, and that split is yours to make.

Rebased onto current main today; all 57 allowlisted paths still resolve in en.json. Happy to split the three groups into separate PRs if you would rather take them one at a time.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The plugin translation allowlist now includes development search, experimental status, installation-dialog, log, and runtime-state paths. Tests cover every exempt path and confirm that adjacent security-sensitive settings and installation fields remain protected.

Mergeability Score: 🔵 Low · up to 1d44f

The PR allows language packs to translate additional non-trust plugin labels, but an accidental removal of an allowlisted path could go undetected and leave part of the UI untranslated. This bounded test-integrity risk is mergeable with explicit owner follow-up.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the changes and testing well, but it omits the required linked issue and does not provide the required before-and-after visual proof. Add a valid issue reference after “Fixes #” and attach before-and-after screenshots or videos, or state N/A with a reason if no interaction changes exist.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main allowlist expansion for plugin install-form and log-control translations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/shared/plugins/plugin-language-pack-artifact.test.ts (1)

64-101: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Cover every new allowlist path and documented protected sibling.

The positive cases omit plugins.search.development, ten PluginInstallDialog paths, and seven PluginSettingsRow paths. The rejection cases omit blocked, bundled, and dev, although the policy comment declares them protected.

Add parameterized cases for these paths. This will lock the translation boundary against namespace and allowlist regressions.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 860a4676-21dd-4b75-a42c-82e4ff046434

📥 Commits

Reviewing files that changed from the base of the PR and between 0ae0fd0 and 4b961a0.

📒 Files selected for processing (2)
  • src/shared/plugins/plugin-language-pack-artifact.test.ts
  • src/shared/plugins/plugin-translatable-chrome.ts

CodeRabbit noticed the positive cases sampled the allowlist — nine of the
fifty-seven paths — and that `blocked`, `bundled` and `dev` are declared
protected in the policy comment without a test holding them there.

The positive side is now parameterised on `translatablePluginChromePaths()`,
so every exempt path is walked end to end through the parser and a path added
later is covered on the day it lands. This is not a restatement of the list:
membership does not make a path reachable, because the walk rejects its
protected container first unless the container stays walkable. Breaking
`translatablePluginChromeContainer` fails 57 of these; the sampled version
missed it.

The three trust badges join the refusal cases, so leaking any of them into the
allowlist fails instead of merely contradicting a comment.
@smwbev

smwbev commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai — both halves taken, one of them differently than suggested.

The refusal cases were a real gap. blocked, bundled and dev were declared protected in the policy comment with nothing holding them there, so widening the allowlist to include them would have contradicted a comment and still passed CI. They are refusal cases now. Checked by mutation: adding those three paths to the allowlist fails exactly three tests.

The positive cases are now driven off the list instead of enumerated. Writing the missing paths out by hand would mirror the allowlist into the test file, and that mirror rots on its own — it only ever covers what someone remembered to copy across. The positive case is parameterised on translatablePluginChromePaths() instead, so all 57 exempt paths are walked end to end through the parser and a path added later is covered on the day it lands.

That is not a restatement of the list. Set membership does not make a path reachable: the walk rejects a protected container before it reaches the leaf, so a path only parses while translatablePluginChromeContainer keeps every level above it walkable. Mutating that function to return false fails 57 of these cases — the nine-path sample missed it entirely.

Existence of each path in en.json is pinned separately for the whole list in src/renderer/src/i18n/plugin-chrome-allowlist.test.ts, which is why the assertions here exercise the parser rather than the catalog.

pnpm lint and pnpm typecheck green; src/shared/plugins and src/renderer/src/i18n are 362 tests passing. Commit 1d44fa6.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline 1d44fa6
printf '\n--- changed paths ---\n'
git show --format='' --name-only 1d44fa6

printf '\n--- implementation outline ---\n'
ast-grep outline src/shared/plugins/plugin-translatable-chrome.ts --items all

printf '\n--- relevant test outline ---\n'
ast-grep outline src/shared/plugins/plugin-language-pack-artifact.test.ts --items all
ast-grep outline src/renderer/src/i18n/plugin-chrome-allowlist.test.ts --items all

printf '\n--- policy, allowlist, and test assertions ---\n'
rg -n -C 5 'blocked|bundled|dev|translatablePluginChromePaths|translatablePluginChromeContainer|policy|protected' \
  src/shared/plugins/plugin-translatable-chrome.ts \
  src/shared/plugins/plugin-language-pack-artifact.test.ts \
  src/renderer/src/i18n/plugin-chrome-allowlist.test.ts

Length of output: 24848


@smwbev Verified in 1d44fa6.

The refusal tests now protect PluginSettingsRow.blocked, PluginSettingsRow.bundled, PluginSettingsRow.dev, and PluginInstallDialog.gitRefRequired.

The positive test now iterates over translatablePluginChromePaths() and validates each path through the parser. This covers protected-container reachability, not only allowlist membership.

plugin-chrome-allowlist.test.ts separately verifies that every allowlisted path exists in en.json. The policy comment remains intact.

The prior findings are addressed. 🐇✅


✏️ Learnings added
Learnt from: smwbev
URL: https://github.com/stablyai/orca/pull/14121

Timestamp: 2026-08-13T00:12:48.464Z
Learning: For the plugin language-pack translation boundary, `src/shared/plugins/plugin-language-pack-artifact.test.ts` must parameterize parser-acceptance tests with `translatablePluginChromePaths()` so each allowlisted path proves both leaf membership and protected-container reachability. `src/renderer/src/i18n/plugin-chrome-allowlist.test.ts` separately checks that every allowlisted path resolves in `en.json`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: smwbev
Repo: stablyai/orca PR: 0
File: :0-0
Timestamp: 2026-08-04T09:06:13.990Z
Learning: In the plugin language-pack translation boundary, the policy comment in `src/shared/plugins/plugin-translatable-chrome.ts` is a design record for deliberate security exclusions. Retain this comment when it documents trust badges, behavior promises, `*Failed` messages, and destructive confirmations.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 755de4e3-6c99-48be-8840-8e9b20442d30

📥 Commits

Reviewing files that changed from the base of the PR and between 4b961a0 and 1d44fa6.

📒 Files selected for processing (1)
  • src/shared/plugins/plugin-language-pack-artifact.test.ts

Comment on lines +74 to +80
it.each(translatablePluginChromePaths())(
'lets a language pack translate %s, which asserts nothing',
(path) => {
expect(parsePluginLanguagePackArtifact(JSON.stringify(catalogFor(path, 'Перевод'))).ok).toBe(
true
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add an independent assertion for the expected allowlist.

it.each(translatablePluginChromePaths()) derives every case from the allowlist itself. If a required path is removed from that list, its test case also disappears and the test still passes. Assert the expected 23 new paths independently, while retaining this loop to test parser reachability.

@AmethystLiang

Copy link
Copy Markdown
Contributor

Thanks for the PR! Taking a look

@smwbev

smwbev commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@AmethystLiang — friendly nudge, no rush intended.

You picked this up on 13 Aug ("Taking a look"), so I wanted to check whether anything here is blocking rather than let it sit silently. The PR has stayed green since: mergeable, and CodeRabbit's review closed with both of its points addressed — the refusal cases it flagged were a real gap, so blocked, bundled and dev are now handled explicitly rather than falling through to the generic message.

If you would rather hand it to someone else, or if it needs to wait on something upstream of it, just say so and I will adjust — a redirect is more useful to me than an approval.

For context, the sibling PR #14127 is also green and mergeable again: upstream split source-control-dropdown-items.ts into per-group modules, and I re-attached the extracted copy at its new call sites rather than reverting the split. CodeRabbit re-reviewed it in full afterwards with no findings.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants