diff --git a/apps/web/components/settings/executor-copy.test.ts b/apps/web/components/settings/executor-copy.test.ts
index f7a4077fea..37c7c0eda4 100644
--- a/apps/web/components/settings/executor-copy.test.ts
+++ b/apps/web/components/settings/executor-copy.test.ts
@@ -134,6 +134,10 @@ describe("create-page executor type registry", () => {
describe("getExecutorDescription", () => {
it.each([
+ // `local` is the SEEDED Executor.type and reached this helper unmapped
+ // before #2249's follow-up, so a local profile's header read "Custom
+ // executor." Both spellings must resolve to the same sentence.
+ ["local", "Runs agents directly in the repository folder."],
["local_pc", "Runs agents directly in the repository folder."],
["worktree", "Creates git worktrees for isolated agent sessions."],
["local_docker", "Runs Docker containers on this machine."],
diff --git a/apps/web/components/settings/executor-description.ts b/apps/web/components/settings/executor-description.ts
index 85be4967b8..2392cf1a26 100644
--- a/apps/web/components/settings/executor-description.ts
+++ b/apps/web/components/settings/executor-description.ts
@@ -10,7 +10,10 @@ import type { ExecutorType } from "@/lib/types/http";
* create page — see the guard comment in eslint.i18n.options.mjs.
*/
export function getExecutorDescription(type: ExecutorType): string {
- if (type === "local_pc") return t("executors:descriptionLocalPc");
+ // Both spellings reach this helper: `local` is the seeded `Executor.type`
+ // (task/repository/sqlite/defaults.go) and `local_pc` comes from the Office
+ // meta listing and the SQLite backfill — see lib/types/executor.ts.
+ if (type === "local" || type === "local_pc") return t("executors:descriptionLocalPc");
if (type === "worktree") return t("executors:descriptionWorktree");
if (type === "local_docker") return t("executors:descriptionLocalDocker");
if (type === "remote_docker") return t("executors:descriptionRemoteDocker");
diff --git a/apps/web/components/settings/plugins/plugin-manifest-card.tsx b/apps/web/components/settings/plugins/plugin-manifest-card.tsx
index 71696db589..bd6cdc6e82 100644
--- a/apps/web/components/settings/plugins/plugin-manifest-card.tsx
+++ b/apps/web/components/settings/plugins/plugin-manifest-card.tsx
@@ -34,7 +34,7 @@ export function PluginManifestCard({ plugin }: { plugin: PluginRecord }) {
{/* Row VALUES are manifest data — the plugin id, its version, its
author — and are never translated. Only the row labels are copy. */}
-
+
diff --git a/apps/web/components/task/mobile/session-task-switcher-sheet.tsx b/apps/web/components/task/mobile/session-task-switcher-sheet.tsx
index ff3aff0aa7..a64f592e21 100644
--- a/apps/web/components/task/mobile/session-task-switcher-sheet.tsx
+++ b/apps/web/components/task/mobile/session-task-switcher-sheet.tsx
@@ -1,6 +1,7 @@
"use client";
import { useCallback, useMemo, useState, memo } from "react";
+import { useTranslation } from "react-i18next";
import { IconMessageCircle, IconPlus } from "@tabler/icons-react";
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@kandev/ui/sheet";
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "@kandev/ui/drawer";
@@ -114,6 +115,8 @@ export function MobileTaskList({
const collapsedSubtaskParents = useAppStore((s) => s.collapsedSubtaskParents);
const toggleSubtaskCollapsed = useAppStore((s) => s.toggleSubtaskCollapsed);
const handleToggleGroup = useSidebarGroupToggle(view.id);
+ // See useGroupedSidebarView: the executorType group label is catalog-backed.
+ const { i18n } = useTranslation();
const grouped = useMemo(
() =>
applyView(tasks, view, {
@@ -121,7 +124,7 @@ export function MobileTaskList({
orderedTaskIds,
subtaskOrderByParentId,
}),
- [tasks, view, pinnedTaskIds, orderedTaskIds, subtaskOrderByParentId],
+ [tasks, view, pinnedTaskIds, orderedTaskIds, subtaskOrderByParentId, i18n.language],
);
return (
{children};
+ };
+}
+
+/**
+ * `executorTypeOptions` labels its options through `getExecutorLabel`, which
+ * reads the catalog at call time. The hook memoizes on store data, so without
+ * the active language in its dependency array the labels would keep the
+ * previous locale until a task snapshot happened to change — invisible to the
+ * pseudo-locale, because the text IS translated, just stale.
+ *
+ * The store deliberately stays untouched across the switch: that is the whole
+ * point. If the only thing that changed is the locale and the label follows,
+ * the dependency is doing its job.
+ */
+describe("useFilterValueOptions — locale", () => {
+ afterAll(async () => {
+ if (i18n.language !== "en") await activateLocale("en");
+ });
+
+ it("recomputes executor-type labels on a locale switch with unchanged store data", async () => {
+ await activateLocale("en");
+
+ const { result } = renderHook(
+ () => {
+ const store = useAppStoreApi();
+ return { options: useFilterValueOptions("executorType"), store };
+ },
+ { wrapper: wrapper() },
+ );
+
+ act(() => {
+ result.current.store.getState().hydrate({
+ kanbanMulti: {
+ snapshots: {
+ w1: {
+ workflowId: "w1",
+ workflowName: "W1",
+ steps: [],
+ tasks: [{ id: "t1", primaryExecutorType: "local" }],
+ },
+ },
+ },
+ } as never);
+ });
+
+ const before = result.current.options;
+ expect(before.map((o) => o.value)).toEqual(["local"]);
+ expect(before.map((o) => o.label)).toEqual(["Local"]);
+
+ await act(async () => {
+ await activateLocale("pseudo");
+ });
+
+ const after = result.current.options;
+ // Same option VALUE — it is the persisted executor type, never translated.
+ expect(after.map((o) => o.value)).toEqual(["local"]);
+ expect(after[0].label).not.toBe("Local");
+ expect(after[0].label).toBe(i18n.t("executors:typeLocal"));
+ });
+});
diff --git a/apps/web/components/task/task-session-sidebar-grouped-view.ts b/apps/web/components/task/task-session-sidebar-grouped-view.ts
index 4a980ab193..b991f3af28 100644
--- a/apps/web/components/task/task-session-sidebar-grouped-view.ts
+++ b/apps/web/components/task/task-session-sidebar-grouped-view.ts
@@ -1,6 +1,7 @@
"use client";
import { useMemo } from "react";
+import { useTranslation } from "react-i18next";
import { applyView } from "@/lib/sidebar/apply-view";
import { useEffectiveSidebarView } from "@/hooks/domains/sidebar/use-effective-sidebar-view";
import { useSidebarTaskPrefs } from "@/hooks/domains/sidebar/use-sidebar-task-prefs";
@@ -10,6 +11,10 @@ export function useGroupedSidebarView(displayTasks: TaskSwitcherItem[]) {
const prefs = useSidebarTaskPrefs();
const effectiveView = useEffectiveSidebarView();
const { pinnedTaskIds, orderedTaskIds, subtaskOrderByParentId } = prefs;
+ // `applyGroup`'s executorType label comes from `getExecutorLabel`, which reads
+ // the catalog. Without the language in the deps the group heading keeps the
+ // previous locale until task data changes.
+ const { i18n } = useTranslation();
const grouped = useMemo(
() =>
applyView(displayTasks, effectiveView, {
@@ -17,7 +22,14 @@ export function useGroupedSidebarView(displayTasks: TaskSwitcherItem[]) {
orderedTaskIds,
subtaskOrderByParentId,
}),
- [displayTasks, effectiveView, pinnedTaskIds, orderedTaskIds, subtaskOrderByParentId],
+ [
+ displayTasks,
+ effectiveView,
+ pinnedTaskIds,
+ orderedTaskIds,
+ subtaskOrderByParentId,
+ i18n.language,
+ ],
);
return { grouped, effectiveView, prefs };
}
diff --git a/apps/web/eslint.i18n.options.mjs b/apps/web/eslint.i18n.options.mjs
index 0901440df5..c182d5729c 100644
--- a/apps/web/eslint.i18n.options.mjs
+++ b/apps/web/eslint.i18n.options.mjs
@@ -1311,13 +1311,21 @@ export const i18nGuardFiles = [
//
// `getExecutorLabel` returns catalog text now, so a caller that does not
// subscribe to i18n renders the previous locale until something else makes it
- // re-render. Every consumer this PR owns subscribes, and
- // `use-filter-value-options.ts` (the task sidebar's executor filter) gained
- // `i18n.language` in its memo deps for the same reason. One consumer is left:
- // `lib/sidebar/apply-view.ts` builds a group label from it inside the sidebar
- // view pipeline, which this PR does not own — its labels resolve correctly on
- // load and lag a runtime locale switch until the view recomputes. It belongs
- // to whoever migrates `lib/sidebar`.
+ // re-render. All three consumers are covered: `ProfileCard` subscribes,
+ // `use-filter-value-options.ts` (the task sidebar's executor filter) carries
+ // `i18n.language` in its memo deps, and both `applyView` consumers — the
+ // desktop sidebar and the mobile task-switcher sheet — do the same for
+ // `applyGroup`'s executorType heading in `lib/sidebar/apply-view.ts`.
+ //
+ // This bug class is invisible to BOTH the guard and the pseudo-locale: the
+ // text is translated, just frozen at the previous locale, so it renders
+ // accented and reads as done. `check-module-scope-t.mjs` does not fire
+ // either — the `t()` call sits inside a function, which is correct, since it
+ // does resolve at call time. Only a locale-switch test covers it, which is
+ // what `use-filter-value-options-locale.test.tsx` is for.
+ //
+ // Still English in `lib/sidebar/apply-view.ts` and belonging to whoever
+ // migrates `lib/sidebar`: `UNASSIGNED_LABEL` and `MULTI_REPO_LABEL`.
//
// One deliberate English change, the only one in this PR: the SSH
// running-sessions confirm read "This executor has 3 running session(s)."
diff --git a/apps/web/src/locales/en/plugins.json b/apps/web/src/locales/en/plugins.json
index bd4d4588d3..9ea5790702 100644
--- a/apps/web/src/locales/en/plugins.json
+++ b/apps/web/src/locales/en/plugins.json
@@ -43,6 +43,7 @@
"manifestApiVersion": "API version",
"manifestAuthor": "Author",
"manifestCapabilities": "Capabilities",
+ "manifestId": "ID",
"manifestInstalled": "Installed",
"manifestSigned": "Signed",
"manifestVersion": "Version",
@@ -74,7 +75,7 @@
"selectPlaceholder": "Select...",
"selectedUtilityAgentUnavailable": "Selected utility agent unavailable",
"settings": "Settings",
- "settingsDescription": "Browse the marketplace, then install, enable, disable, and uninstall kandev plugins.",
+ "settingsDescription": "Browse the marketplace, then install, enable, disable, and uninstall Kandev plugins.",
"settingsSaved": "Plugin settings saved",
"settingsSavedReloadFailed": "Settings saved, but reloading them failed — refresh to confirm.",
"sortMostStars": "Most stars",
diff --git a/apps/web/src/locales/pseudo/plugins.json b/apps/web/src/locales/pseudo/plugins.json
index 1071670951..218e0e2c04 100644
--- a/apps/web/src/locales/pseudo/plugins.json
+++ b/apps/web/src/locales/pseudo/plugins.json
@@ -43,6 +43,7 @@
"manifestApiVersion": "ÀƤĨ vēŕśĩōń",
"manifestAuthor": "Àũţĥōŕ",
"manifestCapabilities": "Ćàƥàƀĩĺĩţĩēś",
+ "manifestId": "ĨĎ",
"manifestInstalled": "Ĩńśţàĺĺēď",
"manifestSigned": "Śĩĝńēď",
"manifestVersion": "Vēŕśĩōń",
@@ -74,7 +75,7 @@
"selectPlaceholder": "Śēĺēćţ...",
"selectedUtilityAgentUnavailable": "Śēĺēćţēď ũţĩĺĩţŷ àĝēńţ ũńàvàĩĺàƀĺē",
"settings": "Śēţţĩńĝś",
- "settingsDescription": "Ɓŕōŵśē ţĥē ḿàŕķēţƥĺàćē, ţĥēń ĩńśţàĺĺ, ēńàƀĺē, ďĩśàƀĺē, àńď ũńĩńśţàĺĺ ķàńďēv ƥĺũĝĩńś.",
+ "settingsDescription": "Ɓŕōŵśē ţĥē ḿàŕķēţƥĺàćē, ţĥēń ĩńśţàĺĺ, ēńàƀĺē, ďĩśàƀĺē, àńď ũńĩńśţàĺĺ Ķàńďēv ƥĺũĝĩńś.",
"settingsSaved": "Ƥĺũĝĩń śēţţĩńĝś śàvēď",
"settingsSavedReloadFailed": "Śēţţĩńĝś śàvēď, ƀũţ ŕēĺōàďĩńĝ ţĥēḿ ƒàĩĺēď — ŕēƒŕēśĥ ţō ćōńƒĩŕḿ.",
"sortMostStars": "Ḿōśţ śţàŕś",