Files
fusion/packages/dashboard/app/components/TrackingRepoSelect.tsx
gsxdsm 1e49494bac feat(i18n): full-sweep string migration — 5,930 keys across 5 locales (#1352)
Migration (multi-agent sweep over 216 files, 60 batches):
- Every user-visible dashboard + TUI string moved to t() with the exact
  English inline default (en rendering byte-identical)
- Catalogs merged from per-batch fragments: en/zh-CN/zh-TW/fr/es now
  carry ~5,930 keys each across common/app/errors/cli namespaces;
  CLI bundles regenerated (6 locales incl. ko)

Integration fixes:
- 18 type errors: reserved {{count}} interpolations renamed, malformed
  plural call, hand-rolled t-param types replaced with TFunction<"app">
- 23 lint errors: superseded label constants/helpers removed
- ExecutorStatusBar hook-order violation (keyboard-open early return
  moved below hooks)
- TUI tests wrapped in I18nextProvider (uninitialized fallback renders
  literal {{placeholders}}); dashboard vitest.setup boots a minimal en
  i18next instance for the same reason

Known WIP (next commits): ~457 residual strings across 50 batches,
Korean drafts for swept keys, and a dashboard test-suite pass that is
still being stabilized (~283 failures under investigation — fake-timer
waitFor interaction, likely stale node_modules vs merged lockfile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 19:06:53 -07:00

117 lines
3.3 KiB
TypeScript

import { useEffect, useId, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import "./TrackingRepoSelect.css";
const CUSTOM_VALUE = "__custom__";
export interface TrackingRepoOption {
value: string;
label: string;
source?: string;
}
interface TrackingRepoSelectProps {
id: string;
value: string;
onChange: (value: string) => void;
options: TrackingRepoOption[];
loading?: boolean;
error?: string;
placeholder?: string;
ariaLabel?: string;
}
function normalizeOptions(options: TrackingRepoOption[]): TrackingRepoOption[] {
const byValue = new Map<string, TrackingRepoOption>();
for (const option of options) {
const normalized = option.value.trim();
if (!normalized || byValue.has(normalized)) {
continue;
}
byValue.set(normalized, {
...option,
value: normalized,
label: option.label?.trim() || normalized,
});
}
return [...byValue.values()].sort((a, b) => a.value.localeCompare(b.value));
}
export function TrackingRepoSelect({
id,
value,
onChange,
options,
loading = false,
error,
placeholder = "owner/repo",
ariaLabel,
}: TrackingRepoSelectProps) {
const { t } = useTranslation("app");
const customInputRef = useRef<HTMLInputElement>(null);
const hintId = useId();
const [showCustomInput, setShowCustomInput] = useState(false);
const normalizedOptions = useMemo(() => normalizeOptions(options), [options]);
const selectedOption = normalizedOptions.find((option) => option.value === value);
const selectValue = selectedOption ? selectedOption.value : CUSTOM_VALUE;
useEffect(() => {
setShowCustomInput(selectValue === CUSTOM_VALUE);
}, [selectValue]);
const handleSelectChange = (nextValue: string) => {
if (nextValue === CUSTOM_VALUE) {
setShowCustomInput(true);
requestAnimationFrame(() => {
customInputRef.current?.focus();
});
return;
}
setShowCustomInput(false);
onChange(nextValue);
};
const renderedHint = error ? error : loading ? t("trackingRepoSelect.loadingHint", "Loading detected GitHub remotes…") : null;
return (
<div className="tracking-repo-select">
<select
id={id}
className="select"
value={selectValue}
onChange={(event) => handleSelectChange(event.target.value)}
aria-label={ariaLabel}
aria-describedby={renderedHint ? hintId : undefined}
>
{normalizedOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.source ? `${option.label} — ${option.source}` : option.label}
</option>
))}
<option value={CUSTOM_VALUE}>{t("trackingRepoSelect.customOption", "Custom…")}</option>
</select>
{showCustomInput ? (
<input
ref={customInputRef}
className="input tracking-repo-select__custom-input"
type="text"
value={value}
placeholder={placeholder}
onChange={(event) => onChange(event.target.value)}
aria-describedby={renderedHint ? hintId : undefined}
/>
) : null}
{renderedHint ? (
<small
id={hintId}
className={error ? "tracking-repo-select__hint tracking-repo-select__hint--error" : "tracking-repo-select__hint"}
>
{renderedHint}
</small>
) : null}
</div>
);
}