Skip to content

Commit a1599fb

Browse files
committed
Add language switcher and stabilize tool versions
- Add locale selection to settings and translate key UI strings - Make sidebar cookie writes tolerant of missing cookieStore - Pin package typecheck, fmt, and lint commands to Bunx wrappers
1 parent 0ca3a4b commit a1599fb

14 files changed

Lines changed: 113 additions & 40 deletions

File tree

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"dev:electron": "bun run scripts/dev-electron.mjs",
1010
"build": "tsdown",
1111
"start": "bun run scripts/start-electron.mjs",
12-
"typecheck": "tsc --noEmit",
12+
"typecheck": "bunx tsc@5.7.3 --noEmit",
1313
"test": "vitest run --passWithNoTests",
1414
"smoke-test": "node scripts/smoke-test.mjs",
1515
"release-smoke": "node ../../scripts/release-smoke.ts"

apps/marketing/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"preview": "next start",
1111
"start": "next start",
1212
"lint": "eslint .",
13-
"typecheck": "tsc --noEmit"
13+
"typecheck": "bunx tsc@5.7.3 --noEmit"
1414
},
1515
"dependencies": {
1616
"@hookform/resolvers": "^3.10.0",

apps/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"build": "bun run scripts/build-mobile-shell.mjs",
8-
"typecheck": "tsc --noEmit",
8+
"typecheck": "bunx tsc@5.7.3 --noEmit",
99
"test": "vitest run --passWithNoTests",
1010
"sync": "cap sync",
1111
"open:ios": "cap open ios",

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"build": "node scripts/cli.ts build",
2020
"start": "node dist/index.mjs",
2121
"prepare": "node -e \"process.exit(process.env.CI ? 0 : 1)\" || (node ../../scripts/patch-effect-language-service.ts && node ../../scripts/patch-effect-smol-peer-installs.mjs)",
22-
"typecheck": "tsc --noEmit",
22+
"typecheck": "bunx tsc@5.7.3 --noEmit",
2323
"test": "vitest run"
2424
},
2525
"dependencies": {

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"build": "vite build",
99
"prepare": "node -e \"process.exit(process.env.CI ? 0 : 1)\" || node ../../scripts/patch-effect-language-service.ts",
1010
"preview": "vite preview",
11-
"typecheck": "tsc --noEmit",
11+
"typecheck": "bunx tsc@5.7.3 --noEmit",
1212
"test": "vitest run --passWithNoTests",
1313
"test:browser": "node ../../scripts/run-browser-tests.mjs vitest.browser.config.ts",
1414
"test:browser:install": "playwright install --with-deps chromium"

apps/web/src/components/settings/SettingsRouteContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export function SettingsRouteContextProvider({ children }: { children: ReactNode
106106
? ["PR request changes button"]
107107
: []),
108108
...(settings.timestampFormat !== defaults.timestampFormat ? ["Time format"] : []),
109+
...(settings.locale !== defaults.locale ? ["Language"] : []),
109110
...(settings.showStitchBorder !== defaults.showStitchBorder ? ["Stitch border"] : []),
110111
...(settings.enableAssistantStreaming !== defaults.enableAssistantStreaming
111112
? ["Assistant output"]

apps/web/src/components/settings/SettingsShell.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Button } from "../ui/button";
1717
import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
1818
import { SidebarInset, SidebarTrigger } from "../ui/sidebar";
1919
import { cn } from "../../lib/utils";
20+
import { useT } from "../../i18n/useI18n";
2021

2122
export type SettingsSectionId =
2223
| "general"
@@ -104,6 +105,7 @@ export function SettingsShell({
104105
children: ReactNode;
105106
}) {
106107
const navigate = useNavigate();
108+
const { t } = useT();
107109
const activeItemLabel = useMemo(
108110
() => SETTINGS_NAV_ITEMS.find((item) => item.id === activeItem)?.label ?? "Settings",
109111
[activeItem],
@@ -142,7 +144,7 @@ export function SettingsShell({
142144
disabled={changedSettingLabels.length === 0}
143145
onClick={() => void onRestoreDefaults()}
144146
>
145-
Restore defaults
147+
{t("common.actions.restoreDefaults")}
146148
</Button>
147149
</div>
148150
</div>
@@ -163,7 +165,7 @@ export function SettingsShell({
163165
disabled={changedSettingLabels.length === 0}
164166
onClick={() => void onRestoreDefaults()}
165167
>
166-
Restore defaults
168+
{t("common.actions.restoreDefaults")}
167169
</Button>
168170
</div>
169171
</div>

apps/web/src/components/ui/sidebar.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ type SidebarContextProps = {
3838
toggleSidebar: () => void;
3939
};
4040

41+
type CookieStoreLike = {
42+
set(options: { expires: number; name: string; path: string; value: string }): Promise<void>;
43+
};
44+
4145
type SidebarResizableOptions = {
4246
maxWidth?: number;
4347
minWidth?: number;
@@ -147,7 +151,9 @@ function SidebarProvider({
147151
}
148152

149153
// This sets the cookie to keep the sidebar state.
150-
await cookieStore.set({
154+
const cookieStore = (globalThis as typeof globalThis & { cookieStore?: CookieStoreLike })
155+
.cookieStore;
156+
await cookieStore?.set({
151157
expires: Date.now() + SIDEBAR_COOKIE_MAX_AGE * 1000,
152158
name: SIDEBAR_COOKIE_NAME,
153159
path: "/",

apps/web/src/routes/__root.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { MobileConnectionBanner } from "../components/mobile/MobileConnectionBan
3333
import { MobilePairingScreen } from "../components/mobile/MobilePairingScreen";
3434
import { useMobilePairingState } from "../hooks/useMobilePairingState";
3535
import { I18nProvider } from "../i18n/I18nProvider";
36+
import { useT } from "../i18n/useI18n";
3637
import { VoodooStitches } from "../components/VoodooStitches";
3738

3839
export const Route = createRootRouteWithContext<{
@@ -57,13 +58,16 @@ function RootRouteView() {
5758
}
5859

5960
function RootRouteContent() {
61+
const { t } = useT();
6062
const { isMobileShell, isLoading, pairingState } = useMobilePairingState();
6163

6264
if (isMobileShell && isLoading) {
6365
return (
6466
<div className="flex h-screen flex-col bg-background text-foreground">
6567
<div className="flex flex-1 items-center justify-center">
66-
<p className="text-sm text-muted-foreground">Restoring mobile pairing...</p>
68+
<p className="text-sm text-muted-foreground">
69+
{t("root.loading.restoringMobilePairing")}
70+
</p>
6771
</div>
6872
</div>
6973
);
@@ -78,7 +82,7 @@ function RootRouteContent() {
7882
<div className="flex h-screen flex-col bg-background text-foreground">
7983
<div className="flex flex-1 items-center justify-center">
8084
<p className="text-sm text-muted-foreground">
81-
Connecting to {APP_DISPLAY_NAME} server...
85+
{t("root.loading.connectingServer", { appName: APP_DISPLAY_NAME })}
8286
</p>
8387
</div>
8488
</div>
@@ -107,6 +111,7 @@ function RootRouteErrorView({ error, reset }: ErrorComponentProps) {
107111
}
108112

109113
function RootRouteErrorContent({ error, reset }: ErrorComponentProps) {
114+
const { t } = useT();
110115
const message = errorMessage(error);
111116
const details = errorDetails(error);
112117

@@ -122,23 +127,23 @@ function RootRouteErrorContent({ error, reset }: ErrorComponentProps) {
122127
{APP_DISPLAY_NAME}
123128
</p>
124129
<h1 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">
125-
Something went wrong.
130+
{t("root.error.title")}
126131
</h1>
127132
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">{message}</p>
128133

129134
<div className="mt-5 flex flex-wrap gap-2">
130135
<Button size="sm" onClick={() => reset()}>
131-
Try again
136+
{t("common.actions.tryAgain")}
132137
</Button>
133138
<Button size="sm" variant="outline" onClick={() => window.location.reload()}>
134-
Reload app
139+
{t("common.actions.reloadApp")}
135140
</Button>
136141
</div>
137142

138143
<details className="group mt-5 overflow-hidden rounded-lg border border-border/70 bg-background/55">
139144
<summary className="cursor-pointer list-none px-3 py-2 text-xs font-medium text-muted-foreground">
140-
<span className="group-open:hidden">Show error details</span>
141-
<span className="hidden group-open:inline">Hide error details</span>
145+
<span className="group-open:hidden">{t("root.error.showDetails")}</span>
146+
<span className="hidden group-open:inline">{t("root.error.hideDetails")}</span>
142147
</summary>
143148
<pre className="max-h-56 overflow-auto border-t border-border/70 bg-background/80 px-3 py-2 text-xs text-foreground/85">
144149
{details}

apps/web/src/routes/_chat.settings.index.tsx

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,19 @@ import {
7777
getProviderStatusDescription,
7878
getProviderStatusHeading,
7979
} from "../components/chat/providerStatusPresentation";
80+
import { APP_LOCALE_PREFERENCES } from "../i18n/types";
81+
import { useT } from "../i18n/useI18n";
8082

81-
const TIMESTAMP_FORMAT_LABELS = {
82-
locale: "System default",
83-
"12-hour": "12-hour",
84-
"24-hour": "24-hour",
85-
} as const;
83+
const TIMESTAMP_FORMAT_OPTIONS = [
84+
{ value: "locale", labelKey: "settings.general.timeFormat.option.locale" },
85+
{ value: "12-hour", labelKey: "settings.general.timeFormat.option.12Hour" },
86+
{ value: "24-hour", labelKey: "settings.general.timeFormat.option.24Hour" },
87+
] as const;
88+
89+
const LANGUAGE_OPTIONS = APP_LOCALE_PREFERENCES.map((value) => ({
90+
value,
91+
labelKey: `settings.general.language.option.${value}` as const,
92+
}));
8693

8794
const PR_REVIEW_REQUEST_CHANGES_TONE_OPTIONS: ReadonlyArray<{
8895
value: PrReviewRequestChangesTone;
@@ -435,6 +442,7 @@ function BuildInfoBlock({ label, buildInfo }: { label: string; buildInfo: BuildM
435442
}
436443

437444
function SettingsRouteView() {
445+
const { t } = useT();
438446
const navigate = useNavigate();
439447
const {
440448
settingsState: { settings, defaults, updateSettings },
@@ -709,6 +717,15 @@ function SettingsRouteView() {
709717
[settings, updateSettings],
710718
);
711719

720+
const languageOptionLabel = (value: (typeof APP_LOCALE_PREFERENCES)[number]) =>
721+
t(`settings.general.language.option.${value}`);
722+
723+
const timestampFormatOptionLabel = (value: (typeof TIMESTAMP_FORMAT_OPTIONS)[number]["value"]) =>
724+
t(
725+
TIMESTAMP_FORMAT_OPTIONS.find((option) => option.value === value)?.labelKey ??
726+
"settings.general.timeFormat.option.locale",
727+
);
728+
712729
return (
713730
<SettingsShell
714731
activeItem={activeSection}
@@ -736,6 +753,50 @@ function SettingsRouteView() {
736753
}
737754
/>
738755

756+
<SettingsRow
757+
title={t("settings.general.language.title")}
758+
description={t("settings.general.language.description")}
759+
resetAction={
760+
settings.locale !== defaults.locale ? (
761+
<SettingResetButton
762+
label="language"
763+
onClick={() =>
764+
updateSettings({
765+
locale: defaults.locale,
766+
})
767+
}
768+
/>
769+
) : null
770+
}
771+
control={
772+
<Select
773+
value={settings.locale}
774+
onValueChange={(value) => {
775+
if (!LANGUAGE_OPTIONS.some((option) => option.value === value)) {
776+
return;
777+
}
778+
updateSettings({
779+
locale: value as (typeof APP_LOCALE_PREFERENCES)[number],
780+
});
781+
}}
782+
>
783+
<SelectTrigger
784+
className="w-full sm:w-40"
785+
aria-label={t("settings.general.language.aria")}
786+
>
787+
<SelectValue>{languageOptionLabel(settings.locale)}</SelectValue>
788+
</SelectTrigger>
789+
<SelectPopup align="end" alignItemWithTrigger={false}>
790+
{LANGUAGE_OPTIONS.map((option) => (
791+
<SelectItem hideIndicator key={option.value} value={option.value}>
792+
{languageOptionLabel(option.value)}
793+
</SelectItem>
794+
))}
795+
</SelectPopup>
796+
</Select>
797+
}
798+
/>
799+
739800
<SettingsRow
740801
title="PR request changes button"
741802
description="Choose how prominent the Request changes action looks in pull request review."
@@ -800,27 +861,25 @@ function SettingsRouteView() {
800861
<Select
801862
value={settings.timestampFormat}
802863
onValueChange={(value) => {
803-
if (value !== "locale" && value !== "12-hour" && value !== "24-hour") {
864+
if (!TIMESTAMP_FORMAT_OPTIONS.some((option) => option.value === value)) {
804865
return;
805866
}
806867
updateSettings({
807-
timestampFormat: value,
868+
timestampFormat: value as (typeof TIMESTAMP_FORMAT_OPTIONS)[number]["value"],
808869
});
809870
}}
810871
>
811872
<SelectTrigger className="w-full sm:w-40" aria-label="Timestamp format">
812-
<SelectValue>{TIMESTAMP_FORMAT_LABELS[settings.timestampFormat]}</SelectValue>
873+
<SelectValue>
874+
{timestampFormatOptionLabel(settings.timestampFormat)}
875+
</SelectValue>
813876
</SelectTrigger>
814877
<SelectPopup align="end" alignItemWithTrigger={false}>
815-
<SelectItem hideIndicator value="locale">
816-
{TIMESTAMP_FORMAT_LABELS.locale}
817-
</SelectItem>
818-
<SelectItem hideIndicator value="12-hour">
819-
{TIMESTAMP_FORMAT_LABELS["12-hour"]}
820-
</SelectItem>
821-
<SelectItem hideIndicator value="24-hour">
822-
{TIMESTAMP_FORMAT_LABELS["24-hour"]}
823-
</SelectItem>
878+
{TIMESTAMP_FORMAT_OPTIONS.map((option) => (
879+
<SelectItem hideIndicator key={option.value} value={option.value}>
880+
{timestampFormatOptionLabel(option.value)}
881+
</SelectItem>
882+
))}
824883
</SelectPopup>
825884
</Select>
826885
}

0 commit comments

Comments
 (0)