fix(i18n): localize watcher fallback labels (#2596)

Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
João Salavisa
2026-08-13 01:00:32 +01:00
committed by GitHub
parent 8ab08a4c5b
commit d58a1df32d
37 changed files with 430 additions and 99 deletions
@@ -31,7 +31,7 @@ import {
DEFAULT_ISSUE_WATCH_PROMPT,
} from "@/components/github/issue-watch-placeholders";
import { RepoFilterSelector } from "@/components/github/repo-filter-selector";
import { STEP_DEFAULT, STEP_DEFAULT_LABEL, resolveProfileId } from "@/lib/watcher-profile-default";
import { STEP_DEFAULT, resolveProfileId } from "@/lib/watcher-profile-default";
import type {
RepoFilter,
IssueWatch,
@@ -271,6 +271,7 @@ function IssueAutomationFields({
}) {
const { t } = useTranslation();
const { workflows, agentProfiles, allExecutorProfiles } = useWatchFormData(form.workspaceId);
const stepDefaultLabel = t("common:useStepDefaultOption");
const { steps: workflowSteps, loading: stepsLoading } = useWorkflowSteps(form.workflowId);
const placeholders = useMemo(() => issueWatchPlaceholders(t), [t]);
@@ -302,9 +303,9 @@ function IssueAutomationFields({
description={t("github:optionalFallsBackToStepDefault")}
value={form.agentProfileId || STEP_DEFAULT}
onChange={(v) => setForm((prev) => ({ ...prev, agentProfileId: resolveProfileId(v) }))}
placeholder={STEP_DEFAULT_LABEL}
placeholder={stepDefaultLabel}
items={[
{ id: STEP_DEFAULT, label: STEP_DEFAULT_LABEL },
{ id: STEP_DEFAULT, label: stepDefaultLabel },
...agentProfiles.map((p) => ({
id: p.id,
label: p.label,
@@ -317,9 +318,9 @@ function IssueAutomationFields({
description={t("github:optionalFallsBackToStepDefault")}
value={form.executorProfileId || STEP_DEFAULT}
onChange={(v) => setForm((prev) => ({ ...prev, executorProfileId: resolveProfileId(v) }))}
placeholder={STEP_DEFAULT_LABEL}
placeholder={stepDefaultLabel}
items={[
{ id: STEP_DEFAULT, label: STEP_DEFAULT_LABEL },
{ id: STEP_DEFAULT, label: stepDefaultLabel },
...allExecutorProfiles.map((p) => ({ id: p.id, label: p.name })),
]}
/>
@@ -29,7 +29,7 @@ import {
import { DEFAULT_REVIEW_WATCH_PROMPT } from "@/components/github/review-watch-placeholders";
import { ReviewWatchPromptField } from "@/components/github/review-watch-prompt-field";
import { RepoFilterSelector } from "@/components/github/repo-filter-selector";
import { STEP_DEFAULT, STEP_DEFAULT_LABEL, resolveProfileId } from "@/lib/watcher-profile-default";
import { STEP_DEFAULT, resolveProfileId } from "@/lib/watcher-profile-default";
import type {
RepoFilter,
ReviewWatch,
@@ -433,6 +433,7 @@ function ProfileFields({
executorProfiles: Array<{ id: string; name: string }>;
}) {
const { t } = useTranslation();
const stepDefaultLabel = t("common:useStepDefaultOption");
return (
<div className="grid grid-cols-2 gap-4">
<SelectField
@@ -440,9 +441,9 @@ function ProfileFields({
description={t("github:optionalFallsBackToStepDefault")}
value={form.agentProfileId || STEP_DEFAULT}
onChange={(v) => setForm((prev) => ({ ...prev, agentProfileId: resolveProfileId(v) }))}
placeholder={STEP_DEFAULT_LABEL}
placeholder={stepDefaultLabel}
items={[
{ id: STEP_DEFAULT, label: STEP_DEFAULT_LABEL },
{ id: STEP_DEFAULT, label: stepDefaultLabel },
...agentProfiles.map((p) => ({
id: p.id,
label: p.label,
@@ -469,10 +470,10 @@ function ProfileFields({
}
>
<SelectTrigger className="cursor-pointer">
<SelectValue placeholder={STEP_DEFAULT_LABEL} />
<SelectValue placeholder={stepDefaultLabel} />
</SelectTrigger>
<SelectContent>
<SelectItem value={STEP_DEFAULT}>{STEP_DEFAULT_LABEL}</SelectItem>
<SelectItem value={STEP_DEFAULT}>{stepDefaultLabel}</SelectItem>
{executorProfiles.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.name}
+6 -5
View File
@@ -21,7 +21,7 @@ import { WatcherRepositoryFields } from "@/components/watcher-repository-fields"
import { useSettingsData } from "@/hooks/domains/settings/use-settings-data";
import { useWorkflowSteps, stepPlaceholder } from "@/hooks/use-workflow-steps";
import { useWorkflows } from "@/hooks/use-workflows";
import { STEP_DEFAULT, STEP_DEFAULT_LABEL, resolveProfileId } from "@/lib/watcher-profile-default";
import { STEP_DEFAULT, resolveProfileId } from "@/lib/watcher-profile-default";
import type {
CreateIssueWatchRequest,
CreateReviewWatchRequest,
@@ -205,6 +205,7 @@ type FormFieldsProps = {
function AutomationFields({ kind, form, setForm }: FormFieldsProps) {
const { t } = useTranslation();
const stepDefaultLabel = t("common:useStepDefaultOption");
const data = useDialogData(form.workspaceId, form.workflowId);
return (
<div className="space-y-4">
@@ -247,9 +248,9 @@ function AutomationFields({ kind, form, setForm }: FormFieldsProps) {
onChange={(value) =>
setForm((current) => ({ ...current, agentProfileId: resolveProfileId(value) }))
}
placeholder={STEP_DEFAULT_LABEL}
placeholder={stepDefaultLabel}
items={[
{ id: STEP_DEFAULT, label: STEP_DEFAULT_LABEL },
{ id: STEP_DEFAULT, label: stepDefaultLabel },
...data.agentProfiles.map((item) => ({ id: item.id, label: item.label })),
]}
/>
@@ -260,9 +261,9 @@ function AutomationFields({ kind, form, setForm }: FormFieldsProps) {
onChange={(value) =>
setForm((current) => ({ ...current, executorProfileId: resolveProfileId(value) }))
}
placeholder={STEP_DEFAULT_LABEL}
placeholder={stepDefaultLabel}
items={[
{ id: STEP_DEFAULT, label: STEP_DEFAULT_LABEL },
{ id: STEP_DEFAULT, label: stepDefaultLabel },
...data.executorProfiles.map((item) => ({ id: item.id, label: item.name })),
]}
/>
@@ -31,7 +31,7 @@ import {
jiraIssueWatchPlaceholders,
DEFAULT_JIRA_ISSUE_WATCH_PROMPT,
} from "@/components/jira/jira-issue-watch-placeholders";
import { STEP_DEFAULT, STEP_DEFAULT_LABEL, resolveProfileId } from "@/lib/watcher-profile-default";
import { STEP_DEFAULT, resolveProfileId } from "@/lib/watcher-profile-default";
import { WatcherRepositoryFields } from "@/components/watcher-repository-fields";
import { clearWorkspaceScopedForm } from "@/lib/watcher-repository-default";
import type {
@@ -339,6 +339,7 @@ function AutomationFields({
setForm: React.Dispatch<React.SetStateAction<FormState>>;
}) {
const { t } = useTranslation();
const stepDefaultLabel = t("common:useStepDefaultOption");
const { workflows, agentProfiles, allExecutorProfiles } = useFormData(form.workspaceId);
const { steps, loading: stepsLoading } = useWorkflowSteps(form.workflowId);
return (
@@ -377,9 +378,9 @@ function AutomationFields({
description={t("jira:optionalFallsBackToStepDefault")}
value={form.agentProfileId || STEP_DEFAULT}
onChange={(v) => setForm((p) => ({ ...p, agentProfileId: resolveProfileId(v) }))}
placeholder={STEP_DEFAULT_LABEL}
placeholder={stepDefaultLabel}
items={[
{ id: STEP_DEFAULT, label: STEP_DEFAULT_LABEL },
{ id: STEP_DEFAULT, label: stepDefaultLabel },
...agentProfiles.map((p) => ({
id: p.id,
label: p.label,
@@ -392,9 +393,9 @@ function AutomationFields({
description={t("jira:optionalFallsBackToStepDefault")}
value={form.executorProfileId || STEP_DEFAULT}
onChange={(v) => setForm((p) => ({ ...p, executorProfileId: resolveProfileId(v) }))}
placeholder={STEP_DEFAULT_LABEL}
placeholder={stepDefaultLabel}
items={[
{ id: STEP_DEFAULT, label: STEP_DEFAULT_LABEL },
{ id: STEP_DEFAULT, label: stepDefaultLabel },
...allExecutorProfiles.map((p) => ({ id: p.id, label: p.name })),
]}
/>
@@ -26,7 +26,7 @@ import {
import { SettingsFields } from "./linear-issue-watch-fields";
import { FilterFields, SelectField } from "./linear-issue-watch-filter-fields";
import { linearIssueWatchPlaceholders } from "./linear-issue-watch-placeholders";
import { STEP_DEFAULT, STEP_DEFAULT_LABEL, resolveProfileId } from "@/lib/watcher-profile-default";
import { STEP_DEFAULT, resolveProfileId } from "@/lib/watcher-profile-default";
import { WatcherRepositoryFields } from "@/components/watcher-repository-fields";
import { clearWorkspaceScopedForm } from "@/lib/watcher-repository-default";
import {
@@ -159,6 +159,7 @@ function AutomationFields({
setForm: React.Dispatch<React.SetStateAction<FormState>>;
}) {
const { t } = useTranslation();
const stepDefaultLabel = t("common:useStepDefaultOption");
const { workflows, agentProfiles, allExecutorProfiles } = useFormData(form.workspaceId);
const { steps, loading: stepsLoading } = useWorkflowSteps(form.workflowId);
return (
@@ -197,9 +198,9 @@ function AutomationFields({
description={t("linear:fallsBackToStepDefault")}
value={form.agentProfileId || STEP_DEFAULT}
onChange={(v) => setForm((p) => ({ ...p, agentProfileId: resolveProfileId(v) }))}
placeholder={STEP_DEFAULT_LABEL}
placeholder={stepDefaultLabel}
items={[
{ id: STEP_DEFAULT, label: STEP_DEFAULT_LABEL },
{ id: STEP_DEFAULT, label: stepDefaultLabel },
...agentProfiles.map((p) => ({
id: p.id,
label: p.label,
@@ -212,9 +213,9 @@ function AutomationFields({
description={t("linear:fallsBackToStepDefault")}
value={form.executorProfileId || STEP_DEFAULT}
onChange={(v) => setForm((p) => ({ ...p, executorProfileId: resolveProfileId(v) }))}
placeholder={STEP_DEFAULT_LABEL}
placeholder={stepDefaultLabel}
items={[
{ id: STEP_DEFAULT, label: STEP_DEFAULT_LABEL },
{ id: STEP_DEFAULT, label: stepDefaultLabel },
...allExecutorProfiles.map((p) => ({ id: p.id, label: p.name })),
]}
/>
@@ -1,10 +1,11 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ReactNode } from "react";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
import { StateProvider } from "@/components/state-provider";
import { ChatMessage } from "./chat-message";
import { entityReferenceMarkdown } from "@/lib/entity-references/message-references";
import type { EntityReference } from "@/lib/types/entity-reference";
import { activateLocale } from "@/lib/i18n";
import {
sessionId as toSessionId,
taskId as toTaskId,
@@ -370,6 +371,26 @@ describe("ChatMessage sender badge", () => {
expect(badge?.textContent).toContain("Old title");
});
it("localizes the fallback when no sender task title is available", async () => {
await activateLocale("en");
const { container } = renderWithSender([], {
sender_task_id: "task-deleted",
sender_task_title: "",
});
const badge = container.querySelector(SENDER_BADGE_SELECTOR);
expect(badge?.textContent).toContain("(unknown task)");
await act(async () => {
await activateLocale("pseudo");
});
expect(badge?.textContent).toContain("(ũńķńōŵń ţàśķ)");
await act(async () => {
await activateLocale("en");
});
});
it("uses the live title when it differs from the snapshot", () => {
// The badge re-resolves the title from the kanban store so renames are
// reflected without re-sending the message.
@@ -50,7 +50,7 @@ export function SenderTaskBadge({ sender, size = "sm" }: SenderTaskBadgeProps) {
const liveSessionName = useAppStore((state) =>
sender.sessionId ? (state.taskSessions.items[sender.sessionId]?.name ?? null) : null,
);
const fullTitle = liveTask?.title || sender.snapshotTitle || "(unknown task)";
const fullTitle = liveTask?.title || sender.snapshotTitle || t("task:unknownTaskFallback");
const sessionName = liveSessionName ?? sender.sessionName ?? "";
const truncated = sessionName
? `${truncateTitle(fullTitle)} · ${truncateTitle(sessionName)}`
@@ -0,0 +1,49 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { WatcherRepositoryFields } from "./watcher-repository-fields";
const branchState = vi.hoisted(() => ({ loading: false }));
vi.mock("@/hooks/domains/workspace/use-repositories", () => ({
useRepositories: () => ({ repositories: [] }),
}));
vi.mock("@/hooks/domains/workspace/use-repository-branches", () => ({
useBranches: () => ({ branches: [], isLoading: branchState.loading }),
}));
afterEach(() => {
cleanup();
branchState.loading = false;
});
describe("WatcherRepositoryFields", () => {
it("shows the repository-first placeholder while the branch selector is disabled", () => {
render(
<WatcherRepositoryFields
workspaceId="workspace-1"
repositoryId=""
baseBranch=""
onRepositoryChange={vi.fn()}
onBaseBranchChange={vi.fn()}
/>,
);
expect(screen.getByLabelText("Base Branch").textContent).toContain("Pick a repository first");
});
it("shows the loading placeholder instead of selecting the default branch", () => {
branchState.loading = true;
render(
<WatcherRepositoryFields
workspaceId="workspace-1"
repositoryId="repository-1"
baseBranch=""
onRepositoryChange={vi.fn()}
onBaseBranchChange={vi.fn()}
/>,
);
expect(screen.getByLabelText("Base Branch").textContent).toContain("Loading…");
});
});
@@ -7,9 +7,7 @@ import { useRepositories } from "@/hooks/domains/workspace/use-repositories";
import { useBranches } from "@/hooks/domains/workspace/use-repository-branches";
import {
NO_REPOSITORY,
NO_REPOSITORY_LABEL,
DEFAULT_BRANCH,
DEFAULT_BRANCH_LABEL,
branchPlaceholder,
resolveBaseBranch,
resolveRepositoryId,
@@ -21,7 +19,7 @@ type PickItem = { id: string; label: string };
function PickSelect(props: {
label: string;
description: string;
value: string;
value: string | undefined;
onChange: (v: string) => void;
placeholder: string;
items: PickItem[];
@@ -83,6 +81,15 @@ export function WatcherRepositoryFields({
const { repositories } = useRepositories(workspaceId, !!workspaceId, true);
const branchSource = repositoryId ? ({ kind: "id", workspaceId, repositoryId } as const) : null;
const { branches, isLoading: branchesLoading } = useBranches(branchSource, !!repositoryId);
const noRepositoryLabel = t("common:noRepositoryOption");
const defaultBranchLabel = t("common:repositoryDefaultBranchOption");
const branchPlaceholderLabels = {
defaultBranch: defaultBranchLabel,
loading: t("common:watcherLoadingBranchesPlaceholder"),
pickRepository: t("common:watcherPickRepositoryPlaceholder"),
};
const branchPlaceholderLabel =
branchPlaceholderLabels[branchPlaceholder(repositoryId, branchesLoading)];
// A branch name can appear twice (local + remote tracking, e.g. "main" and
// origin/"main"), which would emit two <SelectItem value="main"> — Radix then
// renders every matching item's text in the trigger ("mainmain") and React
@@ -95,20 +102,20 @@ export function WatcherRepositoryFields({
description={t("common:optionalTheRepositoryTheAgentWorks")}
value={repositoryId || NO_REPOSITORY}
onChange={(v) => onRepositoryChange(resolveRepositoryId(v))}
placeholder={NO_REPOSITORY_LABEL}
placeholder={noRepositoryLabel}
items={[
{ id: NO_REPOSITORY, label: NO_REPOSITORY_LABEL },
{ id: NO_REPOSITORY, label: noRepositoryLabel },
...repositories.map((r) => ({ id: r.id, label: r.name })),
]}
/>
<PickSelect
label={t("common:baseBranch")}
description={t("common:theBaseBranchTheAgentStarts")}
value={baseBranch || DEFAULT_BRANCH}
value={repositoryId && !branchesLoading ? baseBranch || DEFAULT_BRANCH : undefined}
onChange={(v) => onBaseBranchChange(resolveBaseBranch(v))}
placeholder={branchPlaceholder(repositoryId, branchesLoading)}
placeholder={branchPlaceholderLabel}
items={[
{ id: DEFAULT_BRANCH, label: DEFAULT_BRANCH_LABEL },
{ id: DEFAULT_BRANCH, label: defaultBranchLabel },
...uniqueBranchNames.map((name) => ({ id: name, label: name })),
]}
disabled={!repositoryId || branchesLoading}
@@ -422,10 +422,8 @@ const SCREENS: Screen[] = [
// the nav. `components/integrations/**` is now migrated, which cleared
// `drafted-integration-enabled-control.tsx` ("Enabled"/"Disabled"),
// `auth-status-banner.tsx` ("Authenticated", "· checked <relative>") and the
// watcher card's loading state. What is left is
// `components/watcher-repository-fields.tsx` ("Repository", "Base Branch",
// "(no repository)"), `STEP_DEFAULT_LABEL` and `stepPlaceholder` — all shared
// with the un-migrated Azure DevOps surface — plus
// watcher card's loading state. What is left is `stepPlaceholder`, shared
// with the un-migrated Azure DevOps surface, plus
// `components/integrations/settings-section.tsx` chrome and `@kandev/ui`'s
// built-in dialog "Close" label.
//
-1
View File
@@ -5,7 +5,6 @@
// disallows <SelectItem value="">, so the dropdown item carries this sentinel
// value and maps back to "" on change.
export const STEP_DEFAULT = "__step_default__";
export const STEP_DEFAULT_LABEL = "(use step default)";
// Map a select value back to the stored profile id, collapsing the sentinel to
// "" so the payload keeps signalling "use step default".
@@ -2,7 +2,6 @@ import { describe, it, expect } from "vitest";
import {
NO_REPOSITORY,
DEFAULT_BRANCH,
DEFAULT_BRANCH_LABEL,
resolveRepositoryId,
resolveBaseBranch,
clearWorkspaceScopedForm,
@@ -29,15 +28,15 @@ describe("resolveBaseBranch", () => {
describe("branchPlaceholder", () => {
it("prompts to pick a repository first when none is selected", () => {
expect(branchPlaceholder("", false)).toBe("Pick a repository first");
expect(branchPlaceholder("", false)).toBe("pickRepository");
// No repo takes precedence over the loading state.
expect(branchPlaceholder("", true)).toBe("Pick a repository first");
expect(branchPlaceholder("", true)).toBe("pickRepository");
});
it("shows a loading hint while branches stream in", () => {
expect(branchPlaceholder("repo-1", true)).toBe("Loading…");
expect(branchPlaceholder("repo-1", true)).toBe("loading");
});
it("shows the default-branch label once a repo is selected and loaded", () => {
expect(branchPlaceholder("repo-1", false)).toBe(DEFAULT_BRANCH_LABEL);
expect(branchPlaceholder("repo-1", false)).toBe("defaultBranch");
});
});
+6 -6
View File
@@ -11,9 +11,7 @@
// or repository id (which would otherwise let resolve*() silently rewrite a
// user's selection).
export const NO_REPOSITORY = "kandev:no-repository";
export const NO_REPOSITORY_LABEL = "(no repository)";
export const DEFAULT_BRANCH = "kandev:default-branch";
export const DEFAULT_BRANCH_LABEL = "(repository default branch)";
// Map a repository select value back to the stored id, collapsing the sentinel
// to "" so the payload keeps signalling "no repository".
@@ -56,8 +54,10 @@ export function clearWorkspaceScopedForm<
// branchPlaceholder mirrors the workflow-step placeholder pattern: it nudges the
// user to pick a repository first, then shows a loading hint while branches
// stream in.
export function branchPlaceholder(repositoryId: string, loading: boolean): string {
if (!repositoryId) return "Pick a repository first";
if (loading) return "Loading…";
return DEFAULT_BRANCH_LABEL;
export type BranchPlaceholder = "pickRepository" | "loading" | "defaultBranch";
export function branchPlaceholder(repositoryId: string, loading: boolean): BranchPlaceholder {
if (!repositoryId) return "pickRepository";
if (loading) return "loading";
return "defaultBranch";
}
-2
View File
@@ -161,7 +161,6 @@
"profileNameRequiredTitle": "Profile name is required",
"profileNotFound": "Profile not found.",
"profileSettings": "Profile settings",
"profileStartModelSettings": "Profile start model settings",
"promptTokenHint": "<0>{{token}}</0> will be replaced with your task description or follow-up message.",
"ptyConnectionError": "Connection error",
"refresh": "Refresh",
@@ -185,7 +184,6 @@
"scanningForInstalledAgents": "Scanning for installed agents...",
"searchModels": "Search models...",
"searchModes": "Search modes...",
"selectAModel": "Select a model...",
"selectAModelShort": "Select a model",
"selectModel": "Select model...",
"sessionEnded": "Session ended.",
+5
View File
@@ -319,6 +319,8 @@
"reloadTheApplicationToTryAgain": "Reload the application to try again.",
"rename": "Rename",
"repository": "Repository",
"noRepositoryOption": "(no repository)",
"repositoryDefaultBranchOption": "(repository default branch)",
"repositoryName": "Repository name",
"requestFailed": "Request failed",
"reset": "Reset",
@@ -422,6 +424,9 @@
"untitledTask": "Untitled task",
"usefulCommandsAgentSkills": "Useful commands & agent skills",
"useScratchInstead": "Use scratch instead",
"useStepDefaultOption": "(use step default)",
"watcherLoadingBranchesPlaceholder": "Loading…",
"watcherPickRepositoryPlaceholder": "Pick a repository first",
"useThisFolder": "Use this folder",
"utilities": "Utilities",
"viewFullReleaseOnGithub": "View full release on GitHub",
-1
View File
@@ -531,7 +531,6 @@
"thisWorkspaceUsesOneCredentialFor": "This workspace uses one credential for repository sync, watches, background jobs, and managed agent GitHub access. Executor profile credentials can still take precedence.",
"titleIssues": "Issues",
"titlePullRequests": "Pull requests",
"togglePrEventPromptAutomation": "Toggle PR event prompt automation",
"toggleReviewFollowUpAutomation": "Toggle review follow-up automation",
"typeACustomQueryFirst": "Type a custom query first",
"typeOwnerRepoToSearch": "Type {{format}} to search",
-8
View File
@@ -81,7 +81,6 @@
"completeTheRequiredEditorFieldsBefore": "Complete the required editor fields before saving.",
"configChatAgentDescription": "Choose which agent profile to use for the Configuration Chat. This agent can manage your workflows, agent profiles, and MCP configuration.",
"configChatAgentNoDefault": "No default",
"configChatAgentPlaceholder": "Choose an agent profile...",
"configChatAgentTitle": "Configuration Chat Agent",
"configureArchiveSafeguardsAndDefaultsFor": "Configure archive safeguards and defaults for tasks created by agents",
"configureBackendAndExecutionResourceSampling": "Configure backend and execution resource sampling",
@@ -413,8 +412,6 @@
"mergeWithTarget": "Merge with {{target}}",
"messageSubmitKey": "Message Submit Key",
"metrics": "Metrics",
"mode": "Mode",
"model": "Model",
"moveTabLeft": "Move tab left",
"moveTabRight": "Move tab right",
"moveTabToSplit": "Move tab to split",
@@ -682,13 +679,8 @@
"utilityCustomAgentsDescription": "Create your own utility agents with custom prompts.",
"utilityCustomAgentsEmpty": "No custom utility agents.",
"utilityCustomAgentsTitle": "Custom utility agents",
"utilityDefaultModelAriaLabel": "Default utility model settings",
"utilityDefaultModelDescription": "Select the profile used by built-in utility actions. The profile controls the model, permissions, and launch settings.",
"utilityDefaultModelTitle": "Default utility agent model",
"utilityDefaultWithModel": "Default ({{model}})",
"utilityModelNotConfigured": "Not configured",
"utilitySelectAgent": "Select agent...",
"utilitySelectModel": "Select model...",
"utilityAgentProfile": "Agent profile",
"utilitySelectProfile": "Select profile...",
"utilitySearchProfiles": "Search agent profiles...",
+1 -1
View File
@@ -1009,7 +1009,6 @@
"remoteStateLine": "State: {{state}}",
"remoteStatusFailedLine": "Status failed: {{error}}",
"currentPRCommit": "Current PR commit",
"currentPRCommits": "Current PR commits",
"remoteUrlPressEnter": "<0>Remote URL</0>; press Enter to submit it.",
"remove": "Remove",
"remove2": "Remove {{name}}",
@@ -1499,6 +1498,7 @@
"workspaceRepository": "Workspace repository",
"workspaceRequiresConfiguration": "Workspace requires configuration",
"workspaceUnavailable": "Workspace unavailable",
"unknownTaskFallback": "(unknown task)",
"worktree": "Worktree",
"wouldCreateABlockerCycle": "Would create a blocker cycle: {{cycle}}",
"writeAComment": "Write a comment…",
-2
View File
@@ -161,7 +161,6 @@
"profileNameRequiredTitle": "Ƥŕōƒĩĺē ńàḿē ĩś ŕēqũĩŕēď",
"profileNotFound": "Ƥŕōƒĩĺē ńōţ ƒōũńď.",
"profileSettings": "Ƥŕōƒĩĺē śēţţĩńĝś",
"profileStartModelSettings": "Ƥŕōƒĩĺē śţàŕţ ḿōďēĺ śēţţĩńĝś",
"promptTokenHint": "<0>{{token}}</0> ŵĩĺĺ ƀē ŕēƥĺàćēď ŵĩţĥ ŷōũŕ ţàśķ ďēśćŕĩƥţĩōń ōŕ ƒōĺĺōŵ-ũƥ ḿēśśàĝē.",
"ptyConnectionError": "Ćōńńēćţĩōń ēŕŕōŕ",
"refresh": "Ŕēƒŕēśĥ",
@@ -185,7 +184,6 @@
"scanningForInstalledAgents": "Śćàńńĩńĝ ƒōŕ ĩńśţàĺĺēď àĝēńţś...",
"searchModels": "Śēàŕćĥ ḿōďēĺś...",
"searchModes": "Śēàŕćĥ ḿōďēś...",
"selectAModel": "Śēĺēćţ à ḿōďēĺ...",
"selectAModelShort": "Śēĺēćţ à ḿōďēĺ",
"selectModel": "Śēĺēćţ ḿōďēĺ...",
"sessionEnded": "Śēśśĩōń ēńďēď.",
+5
View File
@@ -319,6 +319,8 @@
"reloadTheApplicationToTryAgain": "Ŕēĺōàď ţĥē àƥƥĺĩćàţĩōń ţō ţŕŷ àĝàĩń.",
"rename": "Ŕēńàḿē",
"repository": "Ŕēƥōśĩţōŕŷ",
"noRepositoryOption": "(ńō ŕēƥōśĩţōŕŷ)",
"repositoryDefaultBranchOption": "(ŕēƥōśĩţōŕŷ ďēƒàũĺţ ƀŕàńćĥ)",
"repositoryName": "Ŕēƥōśĩţōŕŷ ńàḿē",
"requestFailed": "Ŕēqũēśţ ƒàĩĺēď",
"reset": "Ŕēśēţ",
@@ -422,6 +424,9 @@
"untitledTask": "Ũńţĩţĺēď ţàśķ",
"usefulCommandsAgentSkills": "Ũśēƒũĺ ćōḿḿàńďś & àĝēńţ śķĩĺĺś",
"useScratchInstead": "Ũśē śćŕàţćĥ ĩńśţēàď",
"useStepDefaultOption": "(ũśē śţēƥ ďēƒàũĺţ)",
"watcherLoadingBranchesPlaceholder": "Ĺōàďĩńĝ…",
"watcherPickRepositoryPlaceholder": "Ƥĩćķ à ŕēƥōśĩţōŕŷ ƒĩŕśţ",
"useThisFolder": "Ũśē ţĥĩś ƒōĺďēŕ",
"utilities": "Ũţĩĺĩţĩēś",
"viewFullReleaseOnGithub": "Vĩēŵ ƒũĺĺ ŕēĺēàśē ōń ĜĩţĤũƀ",
-1
View File
@@ -531,7 +531,6 @@
"thisWorkspaceUsesOneCredentialFor": "Ţĥĩś ŵōŕķśƥàćē ũśēś ōńē ćŕēďēńţĩàĺ ƒōŕ ŕēƥōśĩţōŕŷ śŷńć, ŵàţćĥēś, ƀàćķĝŕōũńď ĵōƀś, àńď ḿàńàĝēď àĝēńţ ĜĩţĤũƀ àććēśś. Ēxēćũţōŕ ƥŕōƒĩĺē ćŕēďēńţĩàĺś ćàń śţĩĺĺ ţàķē ƥŕēćēďēńćē.",
"titleIssues": "Ĩśśũēś",
"titlePullRequests": "Ƥũĺĺ ŕēqũēśţś",
"togglePrEventPromptAutomation": "Ţōĝĝĺē ƤŔ ēvēńţ ƥŕōḿƥţ àũţōḿàţĩōń",
"toggleReviewFollowUpAutomation": "Ţōĝĝĺē ŕēvĩēŵ ƒōĺĺōŵ-ũƥ àũţōḿàţĩōń",
"typeACustomQueryFirst": "Ţŷƥē à ćũśţōḿ qũēŕŷ ƒĩŕśţ",
"typeOwnerRepoToSearch": "Ţŷƥē {{format}} ţō śēàŕćĥ",
@@ -81,7 +81,6 @@
"completeTheRequiredEditorFieldsBefore": "Ćōḿƥĺēţē ţĥē ŕēqũĩŕēď ēďĩţōŕ ƒĩēĺďś ƀēƒōŕē śàvĩńĝ.",
"configChatAgentDescription": "Ćĥōōśē ŵĥĩćĥ àĝēńţ ƥŕōƒĩĺē ţō ũśē ƒōŕ ţĥē Ćōńƒĩĝũŕàţĩōń Ćĥàţ. Ţĥĩś àĝēńţ ćàń ḿàńàĝē ŷōũŕ ŵōŕķƒĺōŵś, àĝēńţ ƥŕōƒĩĺēś, àńď ḾĆƤ ćōńƒĩĝũŕàţĩōń.",
"configChatAgentNoDefault": "Ńō ďēƒàũĺţ",
"configChatAgentPlaceholder": "Ćĥōōśē àń àĝēńţ ƥŕōƒĩĺē...",
"configChatAgentTitle": "Ćōńƒĩĝũŕàţĩōń Ćĥàţ Àĝēńţ",
"configureArchiveSafeguardsAndDefaultsFor": "Ćōńƒĩĝũŕē àŕćĥĩvē śàƒēĝũàŕďś àńď ďēƒàũĺţś ƒōŕ ţàśķś ćŕēàţēď ƀŷ àĝēńţś",
"configureBackendAndExecutionResourceSampling": "Ćōńƒĩĝũŕē ƀàćķēńď àńď ēxēćũţĩōń ŕēśōũŕćē śàḿƥĺĩńĝ",
@@ -413,8 +412,6 @@
"mergeWithTarget": "Ḿēŕĝē ŵĩţĥ {{target}}",
"messageSubmitKey": "Ḿēśśàĝē Śũƀḿĩţ Ķēŷ",
"metrics": "Ḿēţŕĩćś",
"mode": "Ḿōďē",
"model": "Ḿōďēĺ",
"moveTabLeft": "Ḿōvē ţàƀ ĺēƒţ",
"moveTabRight": "Ḿōvē ţàƀ ŕĩĝĥţ",
"moveTabToSplit": "Ḿōvē ţàƀ ţō śƥĺĩţ",
@@ -682,13 +679,8 @@
"utilityCustomAgentsDescription": "Ćŕēàţē ŷōũŕ ōŵń ũţĩĺĩţŷ àĝēńţś ŵĩţĥ ćũśţōḿ ƥŕōḿƥţś.",
"utilityCustomAgentsEmpty": "Ńō ćũśţōḿ ũţĩĺĩţŷ àĝēńţś.",
"utilityCustomAgentsTitle": "Ćũśţōḿ ũţĩĺĩţŷ àĝēńţś",
"utilityDefaultModelAriaLabel": "Ďēƒàũĺţ ũţĩĺĩţŷ ḿōďēĺ śēţţĩńĝś",
"utilityDefaultModelDescription": "Śēĺēćţ ţĥē ƥŕōƒĩĺē ũśēď ƀŷ ƀũĩĺţ-ĩń ũţĩĺĩţŷ àćţĩōńś. Ţĥē ƥŕōƒĩĺē ćōńţŕōĺś ţĥē ḿōďēĺ, ƥēŕḿĩśśĩōńś, àńď ĺàũńćĥ śēţţĩńĝś.",
"utilityDefaultModelTitle": "Ďēƒàũĺţ ũţĩĺĩţŷ àĝēńţ ḿōďēĺ",
"utilityDefaultWithModel": "Ďēƒàũĺţ ({{model}})",
"utilityModelNotConfigured": "Ńōţ ćōńƒĩĝũŕēď",
"utilitySelectAgent": "Śēĺēćţ àĝēńţ...",
"utilitySelectModel": "Śēĺēćţ ḿōďēĺ...",
"utilityAgentProfile": "Àĝēńţ ƥŕōƒĩĺē",
"utilitySelectProfile": "Śēĺēćţ ƥŕōƒĩĺē...",
"utilitySearchProfiles": "Śēàŕćĥ àĝēńţ ƥŕōƒĩĺēś...",
+1 -1
View File
@@ -1009,7 +1009,6 @@
"remoteStateLine": "Śţàţē: {{state}}",
"remoteStatusFailedLine": "Śţàţũś ƒàĩĺēď: {{error}}",
"currentPRCommit": "Ćũŕŕēńţ ƤŔ ćōḿḿĩţ",
"currentPRCommits": "Ćũŕŕēńţ ƤŔ ćōḿḿĩţś",
"remoteUrlPressEnter": "<0>Ŕēḿōţē ŨŔĹ</0>; ƥŕēśś Ēńţēŕ ţō śũƀḿĩţ ĩţ.",
"remove": "Ŕēḿōvē",
"remove2": "Ŕēḿōvē {{name}}",
@@ -1499,6 +1498,7 @@
"workspaceRepository": "Ŵōŕķśƥàćē ŕēƥōśĩţōŕŷ",
"workspaceRequiresConfiguration": "Ŵōŕķśƥàćē ŕēqũĩŕēś ćōńƒĩĝũŕàţĩōń",
"workspaceUnavailable": "Ŵōŕķśƥàćē ũńàvàĩĺàƀĺē",
"unknownTaskFallback": "(ũńķńōŵń ţàśķ)",
"worktree": "Ŵōŕķţŕēē",
"wouldCreateABlockerCycle": "Ŵōũĺď ćŕēàţē à ƀĺōćķēŕ ćŷćĺē: {{cycle}}",
"writeAComment": "Ŵŕĩţē à ćōḿḿēńţ…",
-2
View File
@@ -153,7 +153,6 @@
"profileNameRequiredTitle": "O nome do perfil é obrigatório",
"profileNotFound": "Perfil não encontrado.",
"profileSettings": "Definições do perfil",
"profileStartModelSettings": "Definições do modelo inicial do perfil",
"promptTokenHint": "<0>{{token}}</0> será substituído pela descrição da sua tarefa ou pela mensagem de seguimento.",
"ptyConnectionError": "Erro de ligação",
"refresh": "Atualizar",
@@ -177,7 +176,6 @@
"scanningForInstalledAgents": "À procura de agentes instalados...",
"searchModels": "Pesquisar modelos...",
"searchModes": "Pesquisar modos...",
"selectAModel": "Selecione um modelo...",
"selectAModelShort": "Selecionar um modelo",
"selectModel": "Selecionar modelo...",
"sessionEnded": "A sessão terminou.",
+5
View File
@@ -312,6 +312,8 @@
"reloadTheApplicationToTryAgain": "Recarregue a aplicação para tentar novamente.",
"rename": "Mudar o nome",
"repository": "Repositório",
"noRepositoryOption": "(sem repositório)",
"repositoryDefaultBranchOption": "(ramo predefinido do repositório)",
"repositoryName": "Nome do repositório",
"requestFailed": "O pedido falhou",
"reset": "Repor",
@@ -413,6 +415,9 @@
"untitledTask": "Tarefa sem título",
"usefulCommandsAgentSkills": "Comandos úteis e competências do agente",
"useScratchInstead": "Usar antes uma área temporária",
"useStepDefaultOption": "(usar predefinição do passo)",
"watcherLoadingBranchesPlaceholder": "A carregar…",
"watcherPickRepositoryPlaceholder": "Escolha primeiro um repositório",
"useThisFolder": "Usar esta pasta",
"viewFullReleaseOnGithub": "Ver a versão completa no GitHub",
"waitsForCiAndAutomatedReviews": "Aguarda pelo CI e pelas revisões automáticas e depois corrige as falhas e responde aos comentários.",
-1
View File
@@ -512,7 +512,6 @@
"thisWorkspaceUsesOneCredentialFor": "Esta área de trabalho usa uma credencial para a sincronização de repositórios, as vigilâncias, as tarefas em segundo plano e o acesso ao GitHub dos agentes geridos. As credenciais dos perfis de executor podem continuar a ter precedência.",
"titleIssues": "Issues",
"titlePullRequests": "Pull requests",
"togglePrEventPromptAutomation": "Alternar a automatização de prompts de eventos de PR",
"typeACustomQueryFirst": "Escreva primeiro uma consulta personalizada",
"typeOwnerRepoToSearch": "Escreva {{format}} para pesquisar",
"unableToLoadTheCurrentTask": "Não foi possível carregar a definição atual de acesso das tarefas.",
-8
View File
@@ -79,7 +79,6 @@
"completeTheRequiredEditorFieldsBefore": "Preencha os campos obrigatórios do editor antes de guardar.",
"configChatAgentDescription": "Escolha o perfil de agente a usar no Chat de configuração. Este agente pode gerir os seus fluxos de trabalho, perfis de agente e configuração MCP.",
"configChatAgentNoDefault": "Sem pré-definição",
"configChatAgentPlaceholder": "Escolha um perfil de agente...",
"configChatAgentTitle": "Agente do Chat de configuração",
"configureArchiveSafeguardsAndDefaultsFor": "Configure as salvaguardas de arquivo e as pré-definições das tarefas criadas por agentes",
"configureBackendAndExecutionResourceSampling": "Configure a amostragem de recursos do backend e da execução",
@@ -378,7 +377,6 @@
"manageApiKeysAndCredentialsSecrets": "Faça a gestão das chaves de API e das credenciais. Os segredos são guardados encriptados e injetados nos ambientes dos agentes através das variáveis de ambiente do perfil de executor.",
"globalScope": "Global",
"modelFallbackSuffix": "(alternativo)",
"mode": "Modo",
"modelWithValue": "Modelo: {{model}}",
"profileFallbackWillBeUsed": "O modelo inicial {{model}} já não está disponível; será usado o alternativo {{fallback}}.",
"profileStartModelUnavailable": "O modelo inicial {{model}} já não está disponível; altere-o no perfil do agente.",
@@ -407,7 +405,6 @@
"mergeWithTarget": "Fundir com {{target}}",
"messageSubmitKey": "Tecla de envio de mensagens",
"metrics": "Métricas",
"model": "Modelo",
"moveTabLeft": "Mover o separador para a esquerda",
"moveTabRight": "Mover o separador para a direita",
"moveTabToSplit": "Mover o separador para uma divisão",
@@ -667,13 +664,8 @@
"utilityCustomAgentsDescription": "Crie os seus próprios agentes utilitários com prompts personalizados.",
"utilityCustomAgentsEmpty": "Não há agentes utilitários personalizados.",
"utilityCustomAgentsTitle": "Agentes utilitários personalizados",
"utilityDefaultModelAriaLabel": "Definições do modelo utilitário pré-definido",
"utilityDefaultModelDescription": "Selecione o perfil usado pelas ações utilitárias incorporadas. O perfil define o modelo, as permissões e as definições de execução.",
"utilityDefaultModelTitle": "Modelo pré-definido dos agentes utilitários",
"utilityDefaultWithModel": "Pré-definido ({{model}})",
"utilityModelNotConfigured": "Não configurado",
"utilitySelectAgent": "Selecionar agente...",
"utilitySelectModel": "Selecionar modelo...",
"utilityAgentProfile": "Perfil do agente",
"utilitySelectProfile": "Selecionar perfil...",
"utilitySearchProfiles": "Pesquisar perfis de agente...",
+1 -1
View File
@@ -1009,7 +1009,6 @@
"remoteStateLine": "Estado: {{state}}",
"remoteStatusFailedLine": "Falha ao obter o estado: {{error}}",
"currentPRCommit": "Commit atual do PR",
"currentPRCommits": "Commits atuais do PR",
"remoteUrlPressEnter": "<0>URL remoto</0>; prima Enter para o submeter.",
"remove": "Remover",
"remove2": "Remover {{name}}",
@@ -1499,6 +1498,7 @@
"workspaceRepository": "Repositório da área de trabalho",
"workspaceRequiresConfiguration": "A área de trabalho requer configuração",
"workspaceUnavailable": "Área de trabalho indisponível",
"unknownTaskFallback": "(tarefa desconhecida)",
"worktree": "Worktree",
"wouldCreateABlockerCycle": "Criaria um ciclo de bloqueios: {{cycle}}",
"writeAComment": "Escreva um comentário…",
-2
View File
@@ -161,7 +161,6 @@
"profileNameRequiredTitle": "配置名称为必填项",
"profileNotFound": "未找到配置。",
"profileSettings": "配置设置",
"profileStartModelSettings": "配置启动模型设置",
"promptTokenHint": "<0>{{token}}</0> 将替换为你的任务描述或后续消息。",
"ptyConnectionError": "连接错误",
"refresh": "刷新",
@@ -185,7 +184,6 @@
"scanningForInstalledAgents": "正在扫描已安装的代理…",
"searchModels": "搜索模型…",
"searchModes": "搜索模式…",
"selectAModel": "选择模型…",
"selectAModelShort": "选择模型",
"selectModel": "选择模型...",
"sessionEnded": "会话已结束。",
+5
View File
@@ -319,6 +319,8 @@
"reloadTheApplicationToTryAgain": "重新加载应用以重试。",
"rename": "重命名",
"repository": "仓库",
"noRepositoryOption": "(无仓库)",
"repositoryDefaultBranchOption": "(仓库默认分支)",
"repositoryName": "仓库名称",
"requestFailed": "请求失败",
"reset": "重置",
@@ -422,6 +424,9 @@
"untitledTask": "未命名任务",
"usefulCommandsAgentSkills": "实用命令与代理技能",
"useScratchInstead": "改用临时工作区",
"useStepDefaultOption": "(使用步骤默认值)",
"watcherLoadingBranchesPlaceholder": "加载中…",
"watcherPickRepositoryPlaceholder": "请先选择仓库",
"useThisFolder": "使用此文件夹",
"utilities": "实用工具",
"viewFullReleaseOnGithub": "在 GitHub 上查看完整发行说明",
-1
View File
@@ -529,7 +529,6 @@
"thisWorkspaceUsesOneCredentialFor": "此工作区使用一个凭据执行仓库同步、监控、后台任务和托管代理的 GitHub 访问。执行器配置凭据仍可优先使用。",
"titleIssues": "议题",
"titlePullRequests": "拉取请求",
"togglePrEventPromptAutomation": "切换 PR 事件提示自动化",
"typeACustomQueryFirst": "请先输入自定义查询",
"typeOwnerRepoToSearch": "输入 {{format}} 进行搜索",
"unableToLoadTheCurrentTask": "无法加载当前任务访问设置。",
-8
View File
@@ -81,7 +81,6 @@
"completeTheRequiredEditorFieldsBefore": "保存前请填写必需的编辑器字段。",
"configChatAgentDescription": "选择用于配置聊天的代理配置。此代理可管理你的工作流、代理配置与 MCP 配置。",
"configChatAgentNoDefault": "无默认",
"configChatAgentPlaceholder": "选择代理配置…",
"configChatAgentTitle": "配置聊天代理",
"configureArchiveSafeguardsAndDefaultsFor": "配置代理创建任务时的归档保护和默认设置",
"configureBackendAndExecutionResourceSampling": "配置后端和执行资源采样",
@@ -413,8 +412,6 @@
"mergeWithTarget": "与 {{target}} 合并",
"messageSubmitKey": "消息提交快捷键",
"metrics": "指标",
"mode": "模式",
"model": "模型",
"moveTabLeft": "左移标签页",
"moveTabRight": "右移标签页",
"moveTabToSplit": "将标签页移至拆分区域",
@@ -682,13 +679,8 @@
"utilityCustomAgentsDescription": "使用自定义提示词创建你自己的实用代理。",
"utilityCustomAgentsEmpty": "无自定义实用代理。",
"utilityCustomAgentsTitle": "自定义实用代理",
"utilityDefaultModelAriaLabel": "默认实用模型设置",
"utilityDefaultModelDescription": "选择内置实用操作使用的配置。配置包含模型、权限和启动设置。",
"utilityDefaultModelTitle": "默认实用代理模型",
"utilityDefaultWithModel": "默认({{model}})",
"utilityModelNotConfigured": "未配置",
"utilitySelectAgent": "选择代理…",
"utilitySelectModel": "选择模型…",
"utilityAgentProfile": "代理配置",
"utilitySelectProfile": "选择配置…",
"utilitySearchProfiles": "搜索代理配置…",
+1 -1
View File
@@ -1009,7 +1009,6 @@
"remoteStateLine": "状态:{{state}}",
"remoteStatusFailedLine": "状态失败:{{error}}",
"currentPRCommit": "当前 PR 提交",
"currentPRCommits": "当前 PR 提交",
"remoteUrlPressEnter": "<0>远程 URL</0>; 按 Enter 提交。",
"remove": "移除",
"remove2": "移除 {{name}}",
@@ -1499,6 +1498,7 @@
"workspaceRepository": "工作区仓库",
"workspaceRequiresConfiguration": "工作区需要配置",
"workspaceUnavailable": "工作区不可用",
"unknownTaskFallback": "(未知任务)",
"worktree": "工作树",
"wouldCreateABlockerCycle": "会形成阻塞循环:{{cycle}}",
"writeAComment": "撰写评论…",
@@ -0,0 +1,95 @@
---
spec: docs/specs/i18n-audit-watcher-copy/spec.md
created: 2026-08-12
status: completed
---
# Implementation Plan: Watcher And Task Fallback Localization
## Overview
Separate stable watcher domain values from render-time copy, then update every watcher consumer and
the sender-task fallback. Finish by deleting the 12 audit-confirmed obsolete keys from all catalogs
and running focused tests plus the required i18n audits. The confirmed root cause is that display
labels were exported from plain helper modules as English constants or returned as English strings;
the orphan keys survived UI replacements without source references.
## Frontend
### Watcher profile defaults
- In `apps/web/lib/watcher-profile-default.ts`, retain `STEP_DEFAULT` and `resolveProfileId` as the
untranslated domain contract and remove `STEP_DEFAULT_LABEL`.
- Add a shared catalog key for the step-default display label and resolve it through each dialog's
existing `useTranslation()` call in GitHub issue/review, GitLab issue/review, Jira, and Linear
watcher dialogs.
- Keep the sentinel-to-empty-string payload behavior unchanged and update focused helper/dialog tests
where they assert the rendered option.
### Watcher repository defaults
- In `apps/web/lib/watcher-repository-default.ts`, retain only sentinel/domain behavior and replace
`branchPlaceholder`'s English return values with a locale-neutral state/key result.
- In `apps/web/components/watcher-repository-fields.tsx`, resolve no-repository, default-branch,
repository-first, and loading copy from catalog keys during render. Never translate repository
names, IDs, or branch names.
- Extend `apps/web/lib/watcher-repository-default.test.ts` to prove locale-neutral state selection and
unchanged sentinel normalization before changing production behavior.
### Sender task fallback
- In `apps/web/components/task/chat/messages/sender-task-badge.tsx`, resolve the unknown-task fallback
from `task` catalog copy inside the component.
- Extend the nearest existing chat-message component test to render a sender with no live or snapshot
title and assert the visible fallback and tooltip text.
### Catalog cleanup
- Remove the following confirmed obsolete entries from English, pseudo, Portuguese, and Simplified
Chinese catalogs: `agents:profileStartModelSettings`, `agents:selectAModel`,
`github:togglePrEventPromptAutomation`, `settings:configChatAgentPlaceholder`, `settings:mode`,
`settings:model`, `settings:utilityDefaultModelAriaLabel`, `settings:utilityDefaultWithModel`,
`settings:utilityModelNotConfigured`, `settings:utilitySelectAgent`,
`settings:utilitySelectModel`, and `task:currentPRCommits`.
- The source-history audit ties these keys to superseded profile model controls, profile-based utility
agent pickers, per-PR automation controls, configuration chat selection, core Voice Mode, and an old
Changes header. Current replacement surfaces already use different, more specific keys.
### Mobile parity
This is copy-only work inside existing responsive dialogs and badges. It changes no composition,
touch target, scrolling, navigation, or viewport branch, so focused component/rendered tests satisfy
the mobile-parity exception; no new mobile Playwright scenario is required.
## Tests
- **What:** watcher sentinels remain stable while placeholder selection becomes locale-neutral.
**File:** `apps/web/lib/watcher-profile-default.test.ts` and
`apps/web/lib/watcher-repository-default.test.ts`. **How:** focused Vitest unit assertions.
- **What:** translated watcher defaults appear through representative dialog/shared-field render
paths without changing submitted IDs. **File:** existing watcher dialog tests and, if the shared
fields lack a suitable harness, `apps/web/components/watcher-repository-fields.test.tsx`.
**How:** focused Vitest component tests using the initialized test i18n instance.
- **What:** an absent sender task title renders the translated fallback. **File:**
`apps/web/components/task/chat/messages/chat-message.test.tsx` or the closest existing sender badge
harness. **How:** focused Vitest component assertion.
- **What:** catalogs contain no audit-reported orphan keys and new references are valid. **How:**
`pnpm run i18n:check` and `pnpm run i18n:sweep -- components lib` from `apps/web`.
## Verification Results
- Focused Vitest: 46 tests passed across 7 files, including repository-first and branch-loading rendered placeholder coverage added during PR review.
- CI remediation: 31 focused component tests and `pnpm run typecheck` passed after allowing the disabled branch select to carry an unset presentation value; live locale switching is asserted with exact English and pseudo fallback copy.
- `pnpm run i18n:check`: passed with 0 orphans, pseudo in sync, and 127 unchanged out-of-scope real-locale parity advisories.
- `pnpm run i18n:sweep -- components lib`: completed across 1,850 files; only the two documented prompt-builder plural findings and 91 unrelated review-by-eye candidates remain.
## Implementation Waves And Parallel Candidates
Wave 1:
- [x] [task-01-localize-watcher-and-task-fallbacks](task-01-localize-watcher-and-task-fallbacks.md)
Wave 2:
- [x] [task-02-remove-obsolete-catalog-entries](task-02-remove-obsolete-catalog-entries.md)
Tasks are sequential because both touch shared locale catalogs and the final orphan audit depends on
the new references from Task 01.
@@ -0,0 +1,74 @@
---
id: "01-localize-watcher-and-task-fallbacks"
title: "Localize watcher and task fallbacks"
status: done
wave: 1
depends_on: []
plan: "plan.md"
spec: "../../specs/i18n-audit-watcher-copy/spec.md"
---
# Task 01: Localize Watcher And Task Fallbacks
## Acceptance
- Every GitHub, GitLab, Jira, and Linear watcher profile default resolves localized copy during
render while sentinel values and submitted empty IDs remain unchanged.
- Watcher repository empty/loading/default copy resolves during render while repository and branch
domain values remain untranslated.
- A sender badge with no live or snapshot title renders the localized unknown-task fallback.
## Verification
```bash
cd apps/web && pnpm test -- --run lib/watcher-profile-default.test.ts lib/watcher-repository-default.test.ts components/task/chat/messages/chat-message.test.tsx components/github/review-watch-dialog.test.tsx components/gitlab/issue-watch-dialog.test.tsx components/gitlab/review-watch-dialog.test.tsx
```
## Files likely touched
- `apps/web/lib/watcher-profile-default.ts`
- `apps/web/lib/watcher-profile-default.test.ts`
- `apps/web/lib/watcher-repository-default.ts`
- `apps/web/lib/watcher-repository-default.test.ts`
- `apps/web/components/watcher-repository-fields.tsx`
- focused shared-field test if needed
- `apps/web/components/github/issue-watch-dialog.tsx`
- `apps/web/components/github/review-watch-dialog.tsx`
- `apps/web/components/gitlab/watch-dialog.tsx`
- `apps/web/components/jira/jira-issue-watch-dialog.tsx`
- `apps/web/components/linear/linear-issue-watch-dialog.tsx`
- `apps/web/components/task/chat/messages/sender-task-badge.tsx`
- closest existing sender badge/chat-message test
- affected locale namespace files for newly referenced keys
- this task file and `plan.md`
## Dependencies
None.
## Parallelism
Sequential. The dialogs share helper and catalog contracts.
## Inputs
- Spec scenarios for watcher profile defaults, repository states, and unknown sender tasks.
- Existing sentinel normalization tests and existing watcher dialog component-test patterns.
- Frontend i18n rule forbidding module-scope `t()` and translation of domain values.
## Output contract
Report RED/GREEN evidence, exact key ownership, unchanged sentinel/payload behavior, files changed,
tests run, blockers and risks, then update this task and `plan.md` statuses.
## Results
RED: `cd apps/web && pnpm test -- --run lib/watcher-repository-default.test.ts components/task/chat/messages/chat-message.test.tsx` failed on the three English-returning placeholder states and the pseudo-locale unknown-task fallback. The remaining chat failures were cascading locale leakage after the intended assertion aborted.
GREEN: `cd apps/web && pnpm test -- --run lib/watcher-profile-default.test.ts lib/watcher-repository-default.test.ts components/task/chat/messages/chat-message.test.tsx components/github/review-watch-dialog.test.tsx components/gitlab/issue-watch-dialog.test.tsx components/gitlab/review-watch-dialog.test.tsx` passed 44 tests in 6 files.
PR review remediation: `cd apps/web && pnpm test -- --run components/watcher-repository-fields.test.tsx lib/watcher-profile-default.test.ts lib/watcher-repository-default.test.ts components/task/chat/messages/chat-message.test.tsx components/github/review-watch-dialog.test.tsx components/gitlab/issue-watch-dialog.test.tsx components/gitlab/review-watch-dialog.test.tsx` passed 46 tests in 7 files after proving that repository-first and loading placeholders remain visible while the branch selector is disabled.
CI/review follow-up: `cd apps/web && pnpm test -- --run components/watcher-repository-fields.test.tsx components/task/chat/messages/chat-message.test.tsx && pnpm run typecheck` passed 31 tests in 2 files and the full web TypeScript check. The sender fallback test now proves an already-mounted badge changes from exact English to exact pseudo copy after a live locale switch.
Sentinel values and empty-ID normalization are unchanged. Display copy resolves inside React render paths; no external side effects or security boundaries apply.
@@ -0,0 +1,62 @@
---
id: "02-remove-obsolete-catalog-entries"
title: "Remove obsolete catalog entries"
status: done
wave: 2
depends_on: ["01-localize-watcher-and-task-fallbacks"]
plan: "plan.md"
spec: "../../specs/i18n-audit-watcher-copy/spec.md"
---
# Task 02: Remove Obsolete Catalog Entries
## Acceptance
- All 12 audit-reported keys, confirmed obsolete through current-source and source-history review,
are absent from English, pseudo, Portuguese, and Simplified Chinese catalogs.
- `i18n:check` reports none of those orphan entries and pseudo remains synchronized; existing
real-locale parity advisories are not repaired in this task.
- The required component/lib sweep reports no hardcoded user-facing copy introduced or retained by
this repair.
## Verification
```bash
cd apps/web && pnpm run i18n:check && pnpm run i18n:sweep -- components lib
```
## Files likely touched
- `apps/web/src/locales/en/agents.json`
- `apps/web/src/locales/en/github.json`
- `apps/web/src/locales/en/settings.json`
- `apps/web/src/locales/en/task.json`
- matching files under `apps/web/src/locales/pseudo/`
- matching files under `apps/web/src/locales/pt-pt/`
- matching files under `apps/web/src/locales/zh-cn/`
- this task file and `plan.md`
## Dependencies
Task 01.
## Parallelism
Sequential. It validates shared catalogs after Task 01 adds the new fallback references.
## Inputs
- The source-history classification recorded in `plan.md`.
- Task 01's final catalog key set.
- User constraint excluding the real-locale parity finding.
## Output contract
Report the removed key list, exact checker/sweep results including unchanged advisory count, files
changed, blockers and risks, then update this task and `plan.md` statuses.
## Results
`cd apps/web && pnpm run i18n:check && pnpm run i18n:sweep -- components lib` passed. The checker reports 0 orphan entries, pseudo in sync, and the unchanged 127 out-of-scope Portuguese/Chinese parity advisories. The sweep scanned 1,850 files and reported only the two documented prompt-builder plural concatenations plus 91 review-by-eye literals; none is introduced or retained by this repair.
All 12 named obsolete keys were removed from English, pseudo, Portuguese, and Simplified Chinese catalogs. External side effects and security/trust boundaries: None.
@@ -0,0 +1,46 @@
---
status: draft
created: 2026-08-12
owner: Kandev
---
# Watcher And Task Fallback Localization
## Why
Watcher dialogs and inter-task message badges expose several English-only fallback labels even
when another locale is active. Obsolete catalog entries also obscure whether the current UI is
fully covered by localization checks.
## What
- Watcher profile selectors display the localized equivalent of `(use step default)` while their
stored sentinel and submitted empty profile ID remain locale-independent.
- Watcher repository and branch selectors localize the no-repository option, default-branch option,
repository-first prompt, and loading prompt at render time. Repository IDs, branch names, and
sentinel values remain untranslated.
- Inter-task sender badges display a localized unknown-task fallback when neither a live title nor a
snapshot title exists.
- English catalog entries with no current source reference and no current user-interface owner are
removed from every catalog. Current behavior is not changed merely to retain an obsolete key.
- Translation lookup does not occur at module scope.
## Scenarios
- **GIVEN** any GitHub issue/review, GitLab issue/review, Jira, or Linear watcher dialog is rendered
in a non-English locale, **WHEN** no agent or executor profile override is selected, **THEN** the
step-default option and placeholder use that locale while saving still submits an empty profile ID.
- **GIVEN** a watcher repository picker is rendered in a non-English locale, **WHEN** repository and
branch selection move through empty, loading, and loaded states, **THEN** only UI fallback copy is
translated and all repository/branch domain values remain unchanged.
- **GIVEN** an inter-task message has no resolvable live or snapshot task title, **WHEN** its sender
badge renders, **THEN** the badge and tooltip use the localized unknown-task fallback.
- **GIVEN** the catalogs contain the 12 audit-reported unreferenced keys, **WHEN** the i18n checker
runs after cleanup, **THEN** it reports no orphan entries from that set and pseudo remains in sync.
## Out of scope
- Resolving the existing Portuguese and Simplified Chinese catalog parity advisories.
- Changing watcher persistence, payload contracts, repository selection behavior, dialog layout, or
mobile interaction behavior.
- Restoring UI that was previously removed or replaced solely to consume an old catalog key.