= ({
}}
className="h-6 px-2 text-xs"
>
- Save
+ {intl.formatMessage(i18n.save)}
= ({
}}
className="h-6 px-2 text-xs"
>
- Cancel
+ {intl.formatMessage(i18n.cancel)}
) : (
{
setEditingMessage(message.id);
if (editingMessageIdRef) editingMessageIdRef.current = message.id;
@@ -385,8 +472,8 @@ export const MessageQueue: React.FC = ({
}`}
title={
editingMessage === message.id
- ? 'Cannot send while editing'
- : 'Stop current processing and send this message now'
+ ? intl.formatMessage(i18n.cannotSendWhileEditing)
+ : intl.formatMessage(i18n.stopAndSend)
}
>
@@ -399,7 +486,7 @@ export const MessageQueue: React.FC = ({
size="sm"
onClick={() => onRemoveMessage(message.id)}
className="opacity-60 hover:opacity-100 transition-opacity h-6 w-6 p-0 hover:bg-destructive/20 hover:text-destructive rounded-full"
- title="Remove this message from queue"
+ title={intl.formatMessage(i18n.removeFromQueue)}
>
@@ -414,7 +501,7 @@ export const MessageQueue: React.FC = ({
{/* Next up indicator */}
{index === 0 && !isPaused && (
- Next
+ {intl.formatMessage(i18n.next)}
)}
@@ -425,7 +512,7 @@ export const MessageQueue: React.FC = ({
{onReorderMessages && queuedMessages.length > 1 && (
- Drag messages to reorder priority
+ {intl.formatMessage(i18n.dragToReorder)}
)}
diff --git a/ui/desktop/src/components/ModelAndProviderContext.tsx b/ui/desktop/src/components/ModelAndProviderContext.tsx
index a006e1c3..42df0578 100644
--- a/ui/desktop/src/components/ModelAndProviderContext.tsx
+++ b/ui/desktop/src/components/ModelAndProviderContext.tsx
@@ -8,13 +8,34 @@ import {
getModelDisplayName,
getProviderDisplayName,
} from './settings/models/predefinedModelsUtils';
+import { defineMessages, useIntl } from '../i18n';
-export const UNKNOWN_PROVIDER_TITLE = 'Provider name lookup';
-export const UNKNOWN_PROVIDER_MSG = 'Unknown provider in config -- please inspect your config.yaml';
-
-// success
-const CHANGE_MODEL_TOAST_TITLE = 'Model changed';
-const SWITCH_MODEL_SUCCESS_MSG = 'Successfully switched models';
+const i18n = defineMessages({
+ unknownProviderTitle: {
+ id: 'modelAndProviderContext.unknownProviderTitle',
+ defaultMessage: 'Provider name lookup',
+ },
+ unknownProviderMsg: {
+ id: 'modelAndProviderContext.unknownProviderMsg',
+ defaultMessage: 'Unknown provider in config -- please inspect your config.yaml',
+ },
+ modelChangedTitle: {
+ id: 'modelAndProviderContext.modelChangedTitle',
+ defaultMessage: 'Model changed',
+ },
+ switchModelSuccess: {
+ id: 'modelAndProviderContext.switchModelSuccess',
+ defaultMessage: 'Successfully switched models -- using {model} from {provider}',
+ },
+ modelChangeFailed: {
+ id: 'modelAndProviderContext.modelChangeFailed',
+ defaultMessage: '{provider}/{model} failed',
+ },
+ selectModel: {
+ id: 'modelAndProviderContext.selectModel',
+ defaultMessage: 'Select Model',
+ },
+});
interface ModelAndProviderContextType {
currentModel: string | null;
@@ -34,10 +55,13 @@ interface ModelAndProviderProviderProps {
const ModelAndProviderContext = createContext(undefined);
+export { i18n as modelAndProviderMessages };
+
export const ModelAndProviderProvider: React.FC = ({ children }) => {
const [currentModel, setCurrentModel] = useState(null);
const [currentProvider, setCurrentProvider] = useState(null);
const { read, getProviders } = useConfig();
+ const intl = useIntl();
const changeModel = useCallback(async (sessionId: string | null, model: Model) => {
const modelName = model.name;
@@ -79,20 +103,23 @@ export const ModelAndProviderProvider: React.FC =
}
toastSuccess({
- title: CHANGE_MODEL_TOAST_TITLE,
- msg: `${SWITCH_MODEL_SUCCESS_MSG} -- using ${model.alias ?? modelName} from ${model.subtext ?? providerName}`,
+ title: intl.formatMessage(i18n.modelChangedTitle),
+ msg: intl.formatMessage(i18n.switchModelSuccess, {
+ model: model.alias ?? modelName,
+ provider: model.subtext ?? providerName,
+ }),
});
return true;
} catch (error) {
console.error(`Failed to change model at ${phase} step -- ${modelName} ${providerName}`);
toastError({
- title: `${providerName}/${modelName} failed`,
+ title: intl.formatMessage(i18n.modelChangeFailed, { provider: providerName, model: modelName }),
msg: `${error}`,
traceback: errorMessage(error),
});
return false;
}
- }, []);
+ }, [intl]);
const getFallbackModelAndProvider = useCallback(async () => {
const provider = window.appConfig.get('GOOSE_DEFAULT_PROVIDER') as string;
@@ -154,9 +181,9 @@ export const ModelAndProviderProvider: React.FC =
const currentModelName = (await read('GOOSE_MODEL', false)) as string;
return getModelDisplayName(currentModelName);
} catch {
- return 'Select Model';
+ return intl.formatMessage(i18n.selectModel);
}
- }, [read]);
+ }, [read, intl]);
const getCurrentProviderDisplayName = useCallback(async () => {
try {
diff --git a/ui/desktop/src/components/ParameterInputModal.tsx b/ui/desktop/src/components/ParameterInputModal.tsx
index 17a0d170..2888efaf 100644
--- a/ui/desktop/src/components/ParameterInputModal.tsx
+++ b/ui/desktop/src/components/ParameterInputModal.tsx
@@ -2,6 +2,58 @@ import React, { useState, useEffect } from 'react';
import { Parameter } from '../recipe';
import { Button } from './ui/button';
import { getInitialWorkingDir } from '../utils/workingDir';
+import { defineMessages, useIntl } from '../i18n';
+
+const i18n = defineMessages({
+ cancelRecipeSetup: {
+ id: 'parameterInputModal.cancelRecipeSetup',
+ defaultMessage: 'Cancel Recipe Setup',
+ },
+ whatToDo: {
+ id: 'parameterInputModal.whatToDo',
+ defaultMessage: 'What would you like to do?',
+ },
+ backToForm: {
+ id: 'parameterInputModal.backToForm',
+ defaultMessage: 'Back to Parameter Form',
+ },
+ startNewChat: {
+ id: 'parameterInputModal.startNewChat',
+ defaultMessage: 'Start New Chat (No Recipe)',
+ },
+ recipeParameters: {
+ id: 'parameterInputModal.recipeParameters',
+ defaultMessage: 'Recipe Parameters',
+ },
+ selectOption: {
+ id: 'parameterInputModal.selectOption',
+ defaultMessage: 'Select an option...',
+ },
+ select: {
+ id: 'parameterInputModal.select',
+ defaultMessage: 'Select...',
+ },
+ true: {
+ id: 'parameterInputModal.true',
+ defaultMessage: 'True',
+ },
+ false: {
+ id: 'parameterInputModal.false',
+ defaultMessage: 'False',
+ },
+ enterValue: {
+ id: 'parameterInputModal.enterValue',
+ defaultMessage: 'Enter value for {key}...',
+ },
+ cancel: {
+ id: 'parameterInputModal.cancel',
+ defaultMessage: 'Cancel',
+ },
+ startRecipe: {
+ id: 'parameterInputModal.startRecipe',
+ defaultMessage: 'Start Recipe',
+ },
+});
interface ParameterInputModalProps {
parameters: Parameter[];
@@ -16,6 +68,7 @@ const ParameterInputModal: React.FC = ({
onClose,
initialValues,
}) => {
+ const intl = useIntl();
const [inputValues, setInputValues] = useState>({});
const [validationErrors, setValidationErrors] = useState>({});
const [showCancelOptions, setShowCancelOptions] = useState(false);
@@ -91,8 +144,10 @@ const ParameterInputModal: React.FC = ({
{showCancelOptions ? (
// Cancel options modal
-
Cancel Recipe Setup
-
What would you like to do?
+
+ {intl.formatMessage(i18n.cancelRecipeSetup)}
+
+
{intl.formatMessage(i18n.whatToDo)}
handleCancelOption('back-to-form')}
@@ -100,7 +155,7 @@ const ParameterInputModal: React.FC = ({
size="lg"
className="w-full rounded-full"
>
- Back to Parameter Form
+ {intl.formatMessage(i18n.backToForm)}
handleCancelOption('new-chat')}
@@ -108,7 +163,7 @@ const ParameterInputModal: React.FC = ({
size="lg"
className="w-full rounded-full"
>
- Start New Chat (No Recipe)
+ {intl.formatMessage(i18n.startNewChat)}
@@ -116,7 +171,9 @@ const ParameterInputModal: React.FC = ({
// Main parameter form
-
Recipe Parameters
+
+ {intl.formatMessage(i18n.recipeParameters)}
+
diff --git a/ui/desktop/src/components/PopularChatTopics.tsx b/ui/desktop/src/components/PopularChatTopics.tsx
index 3073a065..a2651561 100644
--- a/ui/desktop/src/components/PopularChatTopics.tsx
+++ b/ui/desktop/src/components/PopularChatTopics.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import { FolderTree, MessageSquare, Code } from 'lucide-react';
+import { defineMessages, useIntl } from '../i18n';
interface PopularChatTopicsProps {
append: (text: string) => void;
@@ -12,38 +13,65 @@ interface ChatTopic {
prompt: string;
}
-const POPULAR_TOPICS: ChatTopic[] = [
- {
- id: 'organize-photos',
- icon: ,
- description: 'Organize the photos on my desktop into neat little folders by subject matter',
- prompt: 'Organize the photos on my desktop into neat little folders by subject matter',
+const i18n = defineMessages({
+ heading: {
+ id: 'popularChatTopics.heading',
+ defaultMessage: 'Popular chat topics',
},
- {
- id: 'government-forms',
- icon: ,
- description:
- 'Describe in detail how various forms of government works and rank each by units of geese',
- prompt:
+ start: {
+ id: 'popularChatTopics.start',
+ defaultMessage: 'Start',
+ },
+ organizePhotos: {
+ id: 'popularChatTopics.organizePhotos',
+ defaultMessage:
+ 'Organize the photos on my desktop into neat little folders by subject matter',
+ },
+ governmentForms: {
+ id: 'popularChatTopics.governmentForms',
+ defaultMessage:
'Describe in detail how various forms of government works and rank each by units of geese',
},
- {
- id: 'tamagotchi-game',
- icon: ,
- description:
+ tamagotchiGame: {
+ id: 'popularChatTopics.tamagotchiGame',
+ defaultMessage:
'Develop a tamagotchi game that lives on my computer and follows a pixelated styling',
- prompt: 'Develop a tamagotchi game that lives on my computer and follows a pixelated styling',
},
-];
+});
export default function PopularChatTopics({ append }: PopularChatTopicsProps) {
+ const intl = useIntl();
+
+ const POPULAR_TOPICS: ChatTopic[] = [
+ {
+ id: 'organize-photos',
+ icon: ,
+ description: intl.formatMessage(i18n.organizePhotos),
+ prompt: intl.formatMessage(i18n.organizePhotos),
+ },
+ {
+ id: 'government-forms',
+ icon: ,
+ description: intl.formatMessage(i18n.governmentForms),
+ prompt: intl.formatMessage(i18n.governmentForms),
+ },
+ {
+ id: 'tamagotchi-game',
+ icon: ,
+ description: intl.formatMessage(i18n.tamagotchiGame),
+ prompt: intl.formatMessage(i18n.tamagotchiGame),
+ },
+ ];
+
const handleTopicClick = (prompt: string) => {
append(prompt);
};
return (
-
Popular chat topics
+
+ {intl.formatMessage(i18n.heading)}
+
{POPULAR_TOPICS.map((topic) => (
- Start
+ {intl.formatMessage(i18n.start)}
diff --git a/ui/desktop/src/components/ProgressiveMessageList.tsx b/ui/desktop/src/components/ProgressiveMessageList.tsx
index 3264dcab..ef904b6c 100644
--- a/ui/desktop/src/components/ProgressiveMessageList.tsx
+++ b/ui/desktop/src/components/ProgressiveMessageList.tsx
@@ -15,6 +15,7 @@
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { defineMessages, useIntl } from '../i18n';
import { Message, SystemNotificationContent } from '../api';
import GooseMessage from './GooseMessage';
import UserMessage from './UserMessage';
@@ -31,6 +32,17 @@ import LoadingGoose from './LoadingGoose';
import { ChatType } from '../types/chat';
import { identifyConsecutiveToolCalls, isInChain } from '../utils/toolCallChaining';
+const i18n = defineMessages({
+ loadingMessages: {
+ id: 'progressiveMessageList.loadingMessages',
+ defaultMessage: 'Loading messages... ({renderedCount}/{totalCount})',
+ },
+ searchHint: {
+ id: 'progressiveMessageList.searchHint',
+ defaultMessage: 'Press Cmd/Ctrl+F to load all messages immediately for search',
+ },
+});
+
interface ProgressiveMessageListProps {
messages: Message[];
chat: Pick
;
@@ -66,6 +78,7 @@ export default function ProgressiveMessageList({
onRenderingComplete,
submitElicitationResponse,
}: ProgressiveMessageListProps) {
+ const intl = useIntl();
const [renderedCount, setRenderedCount] = useState(() => {
// Initialize with either all messages (if small) or first batch (if large)
return messages.length <= showLoadingThreshold
@@ -270,9 +283,9 @@ export default function ProgressiveMessageList({
{/* Loading indicator when progressively rendering */}
{isLoading && (
-
+
- Press Cmd/Ctrl+F to load all messages immediately for search
+ {intl.formatMessage(i18n.searchHint)}
)}
diff --git a/ui/desktop/src/components/RecipeHeader.tsx b/ui/desktop/src/components/RecipeHeader.tsx
index 9696e8d8..92ff0edb 100644
--- a/ui/desktop/src/components/RecipeHeader.tsx
+++ b/ui/desktop/src/components/RecipeHeader.tsx
@@ -1,14 +1,24 @@
+import { defineMessages, useIntl } from '../i18n';
+
+const i18n = defineMessages({
+ recipeLabel: {
+ id: 'recipeHeader.recipeLabel',
+ defaultMessage: 'Recipe',
+ },
+});
+
interface RecipeHeaderProps {
title: string;
}
export function RecipeHeader({ title }: RecipeHeaderProps) {
+ const intl = useIntl();
return (
- Recipe {' '}
+ {intl.formatMessage(i18n.recipeLabel)} {' '}
{title}
diff --git a/ui/desktop/src/components/SessionIndicators.tsx b/ui/desktop/src/components/SessionIndicators.tsx
index 85a61014..49842ba1 100644
--- a/ui/desktop/src/components/SessionIndicators.tsx
+++ b/ui/desktop/src/components/SessionIndicators.tsx
@@ -1,5 +1,21 @@
import { AlertCircle, Loader2 } from 'lucide-react';
import React from 'react';
+import { defineMessages, useIntl } from '../i18n';
+
+const i18n = defineMessages({
+ error: {
+ id: 'sessionIndicators.error',
+ defaultMessage: 'Session encountered an error',
+ },
+ streaming: {
+ id: 'sessionIndicators.streaming',
+ defaultMessage: 'Streaming',
+ },
+ newActivity: {
+ id: 'sessionIndicators.newActivity',
+ defaultMessage: 'Has new activity',
+ },
+});
interface SessionIndicatorsProps {
isStreaming: boolean;
@@ -12,12 +28,14 @@ interface SessionIndicatorsProps {
*/
export const SessionIndicators = React.memo
(
({ isStreaming, hasUnread, hasError }) => {
+ const intl = useIntl();
+
if (hasError) {
return (
);
@@ -26,7 +44,7 @@ export const SessionIndicators = React.memo(
if (isStreaming) {
return (
-
+
);
}
@@ -34,7 +52,7 @@ export const SessionIndicators = React.memo(
if (hasUnread) {
return (
);
}
diff --git a/ui/desktop/src/components/TelemetryOptOutModal.tsx b/ui/desktop/src/components/TelemetryOptOutModal.tsx
index 45756f52..5e2adcd6 100644
--- a/ui/desktop/src/components/TelemetryOptOutModal.tsx
+++ b/ui/desktop/src/components/TelemetryOptOutModal.tsx
@@ -6,6 +6,68 @@ import { TELEMETRY_UI_ENABLED } from '../updates';
import { toastService } from '../toasts';
import { useConfig } from './ConfigContext';
import { trackTelemetryPreference } from '../utils/analytics';
+import { defineMessages, useIntl } from '../i18n';
+
+const i18n = defineMessages({
+ configError: {
+ id: 'telemetryOptOutModal.configError',
+ defaultMessage: 'Configuration Error',
+ },
+ configErrorMessage: {
+ id: 'telemetryOptOutModal.configErrorMessage',
+ defaultMessage: 'Failed to check telemetry configuration.',
+ },
+ optIn: {
+ id: 'telemetryOptOutModal.optIn',
+ defaultMessage: 'Yes, share anonymous usage data',
+ },
+ optOut: {
+ id: 'telemetryOptOutModal.optOut',
+ defaultMessage: 'No thanks',
+ },
+ heading: {
+ id: 'telemetryOptOutModal.heading',
+ defaultMessage: 'Help improve goose',
+ },
+ description: {
+ id: 'telemetryOptOutModal.description',
+ defaultMessage:
+ 'Would you like to help improve goose by sharing anonymous usage data? This helps us understand how goose is used and identify areas for improvement.',
+ },
+ whatWeCollect: {
+ id: 'telemetryOptOutModal.whatWeCollect',
+ defaultMessage: 'What we collect:',
+ },
+ collectOs: {
+ id: 'telemetryOptOutModal.collectOs',
+ defaultMessage: 'Operating system, version, and architecture',
+ },
+ collectVersion: {
+ id: 'telemetryOptOutModal.collectVersion',
+ defaultMessage: 'goose version and install method',
+ },
+ collectProvider: {
+ id: 'telemetryOptOutModal.collectProvider',
+ defaultMessage: 'Provider and model used',
+ },
+ collectExtensions: {
+ id: 'telemetryOptOutModal.collectExtensions',
+ defaultMessage: 'Extensions and tool usage counts (names only)',
+ },
+ collectSession: {
+ id: 'telemetryOptOutModal.collectSession',
+ defaultMessage: 'Session metrics (duration, interaction count, token usage)',
+ },
+ collectErrors: {
+ id: 'telemetryOptOutModal.collectErrors',
+ defaultMessage: 'Error types (e.g., "rate_limit", "auth" - no details)',
+ },
+ privacyNote: {
+ id: 'telemetryOptOutModal.privacyNote',
+ defaultMessage:
+ 'We never collect your conversations, code, tool arguments, error messages, or any personal data. You can change this setting anytime in Settings → App.',
+ },
+});
const TELEMETRY_CONFIG_KEY = 'GOOSE_TELEMETRY_ENABLED';
@@ -14,6 +76,7 @@ type TelemetryOptOutModalProps =
| { controlled: true; isOpen: boolean; onClose: () => void };
export default function TelemetryOptOutModal(props: TelemetryOptOutModalProps) {
+ const intl = useIntl();
const { read, upsert } = useConfig();
const isControlled = props.controlled;
const controlledIsOpen = isControlled ? props.isOpen : undefined;
@@ -41,15 +104,15 @@ export default function TelemetryOptOutModal(props: TelemetryOptOutModalProps) {
} catch (error) {
console.error('Failed to check telemetry config:', error);
toastService.error({
- title: 'Configuration Error',
- msg: 'Failed to check telemetry configuration.',
+ title: intl.formatMessage(i18n.configError),
+ msg: intl.formatMessage(i18n.configErrorMessage),
traceback: error instanceof Error ? error.stack || '' : '',
});
}
};
checkTelemetryChoice();
- }, [isControlled, read]);
+ }, [isControlled, read, intl]);
const handleChoice = async (enabled: boolean) => {
setIsLoading(true);
@@ -88,7 +151,7 @@ export default function TelemetryOptOutModal(props: TelemetryOptOutModalProps) {
disabled={isLoading}
className="w-full h-[44px] rounded-lg"
>
- Yes, share anonymous usage data
+ {intl.formatMessage(i18n.optIn)}
- No thanks
+ {intl.formatMessage(i18n.optOut)}
}
@@ -106,25 +169,23 @@ export default function TelemetryOptOutModal(props: TelemetryOptOutModalProps) {
- Help improve goose
+ {intl.formatMessage(i18n.heading)}
- Would you like to help improve goose by sharing anonymous usage data? This helps us
- understand how goose is used and identify areas for improvement.
+ {intl.formatMessage(i18n.description)}
-
What we collect:
+
{intl.formatMessage(i18n.whatWeCollect)}
- Operating system, version, and architecture
- goose version and install method
- Provider and model used
- Extensions and tool usage counts (names only)
- Session metrics (duration, interaction count, token usage)
- Error types (e.g., "rate_limit", "auth" - no details)
+ {intl.formatMessage(i18n.collectOs)}
+ {intl.formatMessage(i18n.collectVersion)}
+ {intl.formatMessage(i18n.collectProvider)}
+ {intl.formatMessage(i18n.collectExtensions)}
+ {intl.formatMessage(i18n.collectSession)}
+ {intl.formatMessage(i18n.collectErrors)}
- We never collect your conversations, code, tool arguments, error messages, or any
- personal data. You can change this setting anytime in Settings → App.
+ {intl.formatMessage(i18n.privacyNote)}
diff --git a/ui/desktop/src/components/ToolApprovalButtons.tsx b/ui/desktop/src/components/ToolApprovalButtons.tsx
index 1b93f7f2..b83e1860 100644
--- a/ui/desktop/src/components/ToolApprovalButtons.tsx
+++ b/ui/desktop/src/components/ToolApprovalButtons.tsx
@@ -1,6 +1,42 @@
import { useState, useEffect } from 'react';
import { Button } from './ui/button';
import { confirmToolAction, Permission } from '../api';
+import { defineMessages, useIntl } from '../i18n';
+
+const i18n = defineMessages({
+ allowOnce: {
+ id: 'toolApprovalButtons.allowOnce',
+ defaultMessage: 'Allow Once',
+ },
+ alwaysAllow: {
+ id: 'toolApprovalButtons.alwaysAllow',
+ defaultMessage: 'Always Allow',
+ },
+ deny: {
+ id: 'toolApprovalButtons.deny',
+ defaultMessage: 'Deny',
+ },
+ allowedOnce: {
+ id: 'toolApprovalButtons.allowedOnce',
+ defaultMessage: 'Allowed once',
+ },
+ alwaysAllowed: {
+ id: 'toolApprovalButtons.alwaysAllowed',
+ defaultMessage: 'Always allowed',
+ },
+ denied: {
+ id: 'toolApprovalButtons.denied',
+ defaultMessage: 'Denied',
+ },
+ deniedOnce: {
+ id: 'toolApprovalButtons.deniedOnce',
+ defaultMessage: 'Denied once',
+ },
+ cancelled: {
+ id: 'toolApprovalButtons.cancelled',
+ defaultMessage: 'Cancelled',
+ },
+});
const globalApprovalState = new Map<
string,
@@ -19,6 +55,7 @@ export interface ToolApprovalData {
}
export default function ToolApprovalButtons({ data }: { data: ToolApprovalData }) {
+ const intl = useIntl();
const { id, toolName, prompt, sessionId, isClicked: initialIsClicked } = data;
const storedState = globalApprovalState.get(id);
@@ -60,11 +97,11 @@ export default function ToolApprovalButtons({ data }: { data: ToolApprovalData }
if (isClicked && decision) {
const statusMessages: Record = {
- allow_once: 'Allowed once',
- always_allow: 'Always allowed',
- always_deny: 'Denied',
- deny_once: 'Denied once',
- cancel: 'Cancelled',
+ allow_once: intl.formatMessage(i18n.allowedOnce),
+ always_allow: intl.formatMessage(i18n.alwaysAllowed),
+ always_deny: intl.formatMessage(i18n.denied),
+ deny_once: intl.formatMessage(i18n.deniedOnce),
+ cancel: intl.formatMessage(i18n.cancelled),
};
return (
@@ -80,7 +117,7 @@ export default function ToolApprovalButtons({ data }: { data: ToolApprovalData }
variant="secondary"
onClick={() => handleAction('allow_once')}
>
- Allow Once
+ {intl.formatMessage(i18n.allowOnce)}
{!prompt && (
handleAction('always_allow')}
>
- Always Allow
+ {intl.formatMessage(i18n.alwaysAllow)}
)}
handleAction('deny_once')}>
- Deny
+ {intl.formatMessage(i18n.deny)}
);
diff --git a/ui/desktop/src/components/ToolCallConfirmation.tsx b/ui/desktop/src/components/ToolCallConfirmation.tsx
index 5ce96bd5..487902cc 100644
--- a/ui/desktop/src/components/ToolCallConfirmation.tsx
+++ b/ui/desktop/src/components/ToolCallConfirmation.tsx
@@ -1,6 +1,18 @@
import { ActionRequired } from '../api';
+import { defineMessages, useIntl } from '../i18n';
import ToolApprovalButtons from './ToolApprovalButtons';
+const i18n = defineMessages({
+ allowToolCall: {
+ id: 'toolConfirmation.allowToolCall',
+ defaultMessage: 'Do you allow this tool call?',
+ },
+ gooseWouldLikeToCall: {
+ id: 'toolConfirmation.gooseWouldLikeToCall',
+ defaultMessage: 'Goose would like to call the above tool. Allow?',
+ },
+});
+
type ToolConfirmationData = Extract;
interface ToolConfirmationProps {
@@ -14,6 +26,7 @@ export default function ToolConfirmation({
isClicked,
actionRequiredContent,
}: ToolConfirmationProps) {
+ const intl = useIntl();
const data = actionRequiredContent.data as ToolConfirmationData;
const { id, toolName, prompt } = data;
@@ -21,8 +34,8 @@ export default function ToolConfirmation({
{prompt
- ? 'Do you allow this tool call?'
- : 'Goose would like to call the above tool. Allow?'}
+ ? intl.formatMessage(i18n.allowToolCall)
+ : intl.formatMessage(i18n.gooseWouldLikeToCall)}
= (
status,
className,
}) => {
+ const intl = useIntl();
const getStatusStyles = () => {
switch (status) {
case 'success':
@@ -33,7 +42,7 @@ export const ToolCallStatusIndicator: React.FC = (
getStatusStyles(),
className
)}
- aria-label={`Tool status: ${status}`}
+ aria-label={intl.formatMessage(i18n.toolStatus, { status })}
/>
);
};
diff --git a/ui/desktop/src/components/ToolCallWithResponse.tsx b/ui/desktop/src/components/ToolCallWithResponse.tsx
index 529c10be..55849980 100644
--- a/ui/desktop/src/components/ToolCallWithResponse.tsx
+++ b/ui/desktop/src/components/ToolCallWithResponse.tsx
@@ -22,6 +22,46 @@ import { CallToolResponse, ContentBlock, EmbeddedResource } from '../api';
import McpAppRenderer from './McpApps/McpAppRenderer';
import ToolApprovalButtons from './ToolApprovalButtons';
+import { defineMessages, useIntl } from '../i18n';
+
+const i18n = defineMessages({
+ mcpUiExperimental: {
+ id: 'toolCallWithResponse.mcpUiExperimental',
+ defaultMessage: 'MCP UI is experimental and may change at any time.',
+ },
+ viewSubagentSession: {
+ id: 'toolCallWithResponse.viewSubagentSession',
+ defaultMessage: 'View subagent session',
+ },
+ toolDetails: {
+ id: 'toolCallWithResponse.toolDetails',
+ defaultMessage: 'Tool Details',
+ },
+ code: {
+ id: 'toolCallWithResponse.code',
+ defaultMessage: 'Code',
+ },
+ output: {
+ id: 'toolCallWithResponse.output',
+ defaultMessage: 'Output',
+ },
+ toolResultAlt: {
+ id: 'toolCallWithResponse.toolResultAlt',
+ defaultMessage: 'Tool result',
+ },
+ activityCount: {
+ id: 'toolCallWithResponse.activityCount',
+ defaultMessage: 'Activity ({count})',
+ },
+ logs: {
+ id: 'toolCallWithResponse.logs',
+ defaultMessage: 'Logs',
+ },
+ loadingSpinner: {
+ id: 'toolCallWithResponse.loadingSpinner',
+ defaultMessage: 'Loading spinner',
+ },
+});
interface ToolGraphNode {
tool: string;
@@ -189,6 +229,7 @@ export default function ToolCallWithResponse({
confirmationContent,
isApprovalClicked,
}: ToolCallWithResponseProps) {
+ const intl = useIntl();
// Handle both the wrapped ToolResult format and the unwrapped format
// The server serializes ToolResult as { status: "success", value: T } or { status: "error", error: string }
const toolCallData = toolRequest.toolCall as Record;
@@ -263,7 +304,7 @@ export default function ToolCallWithResponse({
- MCP UI is experimental and may change at any time.
+ {intl.formatMessage(i18n.mcpUiExperimental)}
@@ -464,6 +505,7 @@ function ToolCallView({
notifications,
isStreamingMessage = false,
}: ToolCallViewProps) {
+ const intl = useIntl();
const [responseStyle, setResponseStyle] = useState('concise');
useEffect(() => {
@@ -854,7 +896,7 @@ function ToolCallView({
className="w-full flex items-center gap-2 px-4 py-2 text-xs text-text-secondary hover:text-text-primary hover:bg-background-secondary transition-colors cursor-pointer"
>
- View subagent session
+ {intl.formatMessage(i18n.viewSubagentSession)}
);
@@ -872,9 +914,10 @@ interface ToolDetailsViewProps {
}
function ToolDetailsView({ toolCall, isStartExpanded }: ToolDetailsViewProps) {
+ const intl = useIntl();
return (
Tool Details}
+ label={{intl.formatMessage(i18n.toolDetails)} }
isStartExpanded={isStartExpanded}
>
@@ -892,6 +935,7 @@ interface CodeModeViewProps {
}
function CodeModeView({ toolGraph, code }: CodeModeViewProps) {
+ const intl = useIntl();
const renderGraph = () => {
const graph = toolGraph ?? [];
if (graph.length === 0) return null;
@@ -915,7 +959,7 @@ function CodeModeView({ toolGraph, code }: CodeModeViewProps) {
{code && (
Code}
+ label={{intl.formatMessage(i18n.code)} }
isStartExpanded={false}
>
'text' in c && typeof (c as Record).text === 'string';
@@ -953,7 +998,7 @@ function ToolResultView({ result, isStartExpanded }: ToolResultViewProps) {
return (
Output}
+ label={{intl.formatMessage(i18n.output)} }
isStartExpanded={isStartExpanded}
>
@@ -965,7 +1010,7 @@ function ToolResultView({ result, isStartExpanded }: ToolResultViewProps) {
{hasImage(result) && (
{
console.error('Failed to load image');
@@ -1018,6 +1063,7 @@ function ToolLogsView({
working: boolean;
isStartExpanded?: boolean;
}) {
+ const intl = useIntl();
const boxRef = useRef
(null);
// Whenever logs update, jump to the newest entry
@@ -1034,7 +1080,9 @@ function ToolLogsView({
// down on the possibility of unwanted runs
const subagentLogCount = logs.filter((l) => l.startsWith('[subagent:')).length;
- const labelText = subagentLogCount > 0 ? `Activity (${subagentLogCount})` : 'Logs';
+ const labelText = subagentLogCount > 0
+ ? intl.formatMessage(i18n.activityCount, { count: subagentLogCount })
+ : intl.formatMessage(i18n.logs);
return (
)}
diff --git a/ui/desktop/src/components/UserMessage.tsx b/ui/desktop/src/components/UserMessage.tsx
index 112a78e7..77a0f446 100644
--- a/ui/desktop/src/components/UserMessage.tsx
+++ b/ui/desktop/src/components/UserMessage.tsx
@@ -7,6 +7,70 @@ import MessageCopyLink from './MessageCopyLink';
import { formatMessageTimestamp } from '../utils/timeUtils';
import Edit from './icons/Edit';
import { Button } from './ui/button';
+import { defineMessages, useIntl } from '../i18n';
+
+const i18n = defineMessages({
+ editPlaceholder: {
+ id: 'userMessage.editPlaceholder',
+ defaultMessage: 'Edit your message...',
+ },
+ editAriaLabel: {
+ id: 'userMessage.editAriaLabel',
+ defaultMessage: 'Edit message content',
+ },
+ emptyError: {
+ id: 'userMessage.emptyError',
+ defaultMessage: 'Message cannot be empty',
+ },
+ editInPlaceDescription: {
+ id: 'userMessage.editInPlaceDescription',
+ defaultMessage: 'Edit in Place updates this session • Fork Session creates a new session',
+ },
+ cancel: {
+ id: 'userMessage.cancel',
+ defaultMessage: 'Cancel',
+ },
+ cancelAriaLabel: {
+ id: 'userMessage.cancelAriaLabel',
+ defaultMessage: 'Cancel editing',
+ },
+ editInPlace: {
+ id: 'userMessage.editInPlace',
+ defaultMessage: 'Edit in Place',
+ },
+ editInPlaceAriaLabel: {
+ id: 'userMessage.editInPlaceAriaLabel',
+ defaultMessage: 'Edit message in place',
+ },
+ editInPlaceTitle: {
+ id: 'userMessage.editInPlaceTitle',
+ defaultMessage: 'Update the message in this session',
+ },
+ forkSession: {
+ id: 'userMessage.forkSession',
+ defaultMessage: 'Fork Session',
+ },
+ forkSessionAriaLabel: {
+ id: 'userMessage.forkSessionAriaLabel',
+ defaultMessage: 'Fork session with edited message',
+ },
+ forkSessionTitle: {
+ id: 'userMessage.forkSessionTitle',
+ defaultMessage: 'Create a new session with the edited message',
+ },
+ editButton: {
+ id: 'userMessage.editButton',
+ defaultMessage: 'Edit',
+ },
+ editMessageAriaLabel: {
+ id: 'userMessage.editMessageAriaLabel',
+ defaultMessage: 'Edit message: {preview}',
+ },
+ editMessageTitle: {
+ id: 'userMessage.editMessageTitle',
+ defaultMessage: 'Edit message',
+ },
+});
interface UserMessageProps {
message: Message;
@@ -14,6 +78,7 @@ interface UserMessageProps {
}
export default function UserMessage({ message, onMessageUpdate }: UserMessageProps) {
+ const intl = useIntl();
const contentRef = useRef(null);
const textareaRef = useRef(null);
const [isEditing, setIsEditing] = useState(false);
@@ -74,7 +139,7 @@ export default function UserMessage({ message, onMessageUpdate }: UserMessagePro
const handleSave = useCallback(
(editType: 'fork' | 'edit' = 'fork') => {
if (editContent.trim().length === 0) {
- setError('Message cannot be empty');
+ setError(intl.formatMessage(i18n.emptyError));
return;
}
@@ -88,7 +153,7 @@ export default function UserMessage({ message, onMessageUpdate }: UserMessagePro
onMessageUpdate(message.id, editContent, editType);
}
},
- [editContent, textContent, onMessageUpdate, message.id]
+ [editContent, textContent, onMessageUpdate, message.id, intl]
);
// Handle cancel action
@@ -147,8 +212,8 @@ export default function UserMessage({ message, onMessageUpdate }: UserMessagePro
wordBreak: 'break-word',
overflowWrap: 'break-word',
}}
- placeholder="Edit your message..."
- aria-label="Edit message content"
+ placeholder={intl.formatMessage(i18n.editPlaceholder)}
+ aria-label={intl.formatMessage(i18n.editAriaLabel)}
aria-describedby={error ? `error-${message.id}` : undefined}
/>
{/* Error message */}
@@ -164,27 +229,28 @@ export default function UserMessage({ message, onMessageUpdate }: UserMessagePro
)}
- Edit in Place updates this session •{' '}
- Fork Session creates a new session
+ {intl.formatMessage(i18n.editInPlaceDescription, {
+ b: (chunks: React.ReactNode) => {chunks} ,
+ })}
-
- Cancel
+
+ {intl.formatMessage(i18n.cancel)}
handleSave('edit')}
variant="secondary"
- aria-label="Edit message in place"
- title="Update the message in this session"
+ aria-label={intl.formatMessage(i18n.editInPlaceAriaLabel)}
+ title={intl.formatMessage(i18n.editInPlaceTitle)}
>
- Edit in Place
+ {intl.formatMessage(i18n.editInPlace)}
handleSave('fork')}
- aria-label="Fork session with edited message"
- title="Create a new session with the edited message"
+ aria-label={intl.formatMessage(i18n.forkSessionAriaLabel)}
+ title={intl.formatMessage(i18n.forkSessionTitle)}
>
- Fork Session
+ {intl.formatMessage(i18n.forkSession)}
@@ -227,12 +293,12 @@ export default function UserMessage({ message, onMessageUpdate }: UserMessagePro
}
}}
className="flex items-center gap-1 text-xs text-text-secondary hover:cursor-pointer hover:text-text-primary transition-all duration-200 opacity-0 group-hover:opacity-100 -translate-y-4 group-hover:translate-y-0 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-50 rounded"
- aria-label={`Edit message: ${textContent.substring(0, 50)}${textContent.length > 50 ? '...' : ''}`}
+ aria-label={intl.formatMessage(i18n.editMessageAriaLabel, { preview: `${textContent.substring(0, 50)}${textContent.length > 50 ? '...' : ''}` })}
aria-expanded={isEditing}
- title="Edit message"
+ title={intl.formatMessage(i18n.editMessageTitle)}
>
- Edit
+ {intl.formatMessage(i18n.editButton)}
diff --git a/ui/desktop/src/components/__tests__/GroupedExtensionLoadingToast.test.tsx b/ui/desktop/src/components/__tests__/GroupedExtensionLoadingToast.test.tsx
index 701e9e04..b28a8806 100644
--- a/ui/desktop/src/components/__tests__/GroupedExtensionLoadingToast.test.tsx
+++ b/ui/desktop/src/components/__tests__/GroupedExtensionLoadingToast.test.tsx
@@ -2,9 +2,14 @@ import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { GroupedExtensionLoadingToast } from '../GroupedExtensionLoadingToast';
+import { IntlTestWrapper } from '../../i18n/test-utils';
const renderWithRouter = (component: React.ReactElement) => {
- return render(
{component} );
+ return render(
+
+ {component}
+
+ );
};
describe('GroupedExtensionLoadingToast', () => {
diff --git a/ui/desktop/src/components/alerts/AlertBox.tsx b/ui/desktop/src/components/alerts/AlertBox.tsx
index f5723d0d..746088a4 100644
--- a/ui/desktop/src/components/alerts/AlertBox.tsx
+++ b/ui/desktop/src/components/alerts/AlertBox.tsx
@@ -6,6 +6,7 @@ import { errorMessage } from '../../utils/conversionUtils';
import { Alert, AlertType } from './types';
import { upsertConfig } from '../../api';
import { useConfig } from '../ConfigContext';
+import { defineMessages, useIntl } from '../../i18n';
const alertIcons: Record
= {
[AlertType.Error]: ,
@@ -19,6 +20,21 @@ interface AlertBoxProps {
compactButtonEnabled?: boolean;
}
+const i18n = defineMessages({
+ autoCompactAt: {
+ id: 'alertBox.autoCompactAt',
+ defaultMessage: 'Auto compact at',
+ },
+ compactNow: {
+ id: 'alertBox.compactNow',
+ defaultMessage: 'Compact now',
+ },
+ failedToSaveThreshold: {
+ id: 'alertBox.failedToSaveThreshold',
+ defaultMessage: 'Failed to save threshold: {error}',
+ },
+});
+
const alertStyles: Record = {
[AlertType.Error]: 'bg-[#d7040e] text-white',
[AlertType.Warning]: 'bg-[#cc4b03] text-white',
@@ -37,6 +53,7 @@ const formatTokenCount = (count: number): string => {
};
export const AlertBox = ({ alert, className }: AlertBoxProps) => {
+ const intl = useIntl();
const { read } = useConfig();
const [isEditingThreshold, setIsEditingThreshold] = useState(false);
const [loadedThreshold, setLoadedThreshold] = useState(0.8);
@@ -90,7 +107,7 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
}
} catch (error) {
console.error('Error saving threshold:', error);
- window.alert(`Failed to save threshold: ${errorMessage(error, 'Unknown error')}`);
+ window.alert(intl.formatMessage(i18n.failedToSaveThreshold, { error: errorMessage(error, 'Unknown error') }));
} finally {
setIsSaving(false);
}
@@ -114,7 +131,7 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
{isEditingThreshold ? (
<>
- Auto compact at
+ {intl.formatMessage(i18n.autoCompactAt)}
{
) : (
<>
- Auto compact at {Math.round(currentThreshold * 100)}%
+ {intl.formatMessage(i18n.autoCompactAt)} {Math.round(currentThreshold * 100)}%
{
)}
>
{alert.compactIcon}
- Compact now
+ {intl.formatMessage(i18n.compactNow)}
)}
diff --git a/ui/desktop/src/components/alerts/__tests__/AlertBox.test.tsx b/ui/desktop/src/components/alerts/__tests__/AlertBox.test.tsx
index f6cbaa29..43776e80 100644
--- a/ui/desktop/src/components/alerts/__tests__/AlertBox.test.tsx
+++ b/ui/desktop/src/components/alerts/__tests__/AlertBox.test.tsx
@@ -1,8 +1,12 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, screen, fireEvent } from '@testing-library/react';
+import { render, type RenderOptions, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AlertBox } from '../AlertBox';
import { Alert, AlertType } from '../types';
+import { IntlTestWrapper } from '../../../i18n/test-utils';
+
+const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) =>
+ render(ui, { wrapper: IntlTestWrapper, ...options });
// Mock the ConfigContext
vi.mock('../../ConfigContext', () => ({
@@ -25,7 +29,7 @@ describe('AlertBox', () => {
message: 'Test info message',
};
- render( );
+ renderWithIntl( );
expect(screen.getByText('Test info message')).toBeInTheDocument();
});
@@ -36,7 +40,7 @@ describe('AlertBox', () => {
message: 'Test warning message',
};
- const { container } = render( );
+ const { container } = renderWithIntl( );
const alertElement = container.querySelector('.bg-\\[\\#cc4b03\\]');
expect(alertElement).toBeInTheDocument();
@@ -49,7 +53,7 @@ describe('AlertBox', () => {
message: 'Test error message',
};
- const { container } = render( );
+ const { container } = renderWithIntl( );
const alertElement = container.querySelector('.bg-\\[\\#d7040e\\]');
expect(alertElement).toBeInTheDocument();
@@ -62,7 +66,7 @@ describe('AlertBox', () => {
message: 'Test message',
};
- const { container } = render( );
+ const { container } = renderWithIntl( );
const alertElement = container.firstChild as HTMLElement;
expect(alertElement).toHaveClass('custom-class');
@@ -80,7 +84,7 @@ describe('AlertBox', () => {
},
};
- render( );
+ renderWithIntl( );
expect(screen.getByText('50')).toBeInTheDocument();
expect(screen.getByText('50%')).toBeInTheDocument();
@@ -103,7 +107,7 @@ describe('AlertBox', () => {
},
};
- render( );
+ renderWithIntl( );
expect(screen.getByText('0')).toBeInTheDocument();
expect(screen.getByText('0%')).toBeInTheDocument();
@@ -120,7 +124,7 @@ describe('AlertBox', () => {
},
};
- render( );
+ renderWithIntl( );
// Use getAllByText since there are multiple "100" elements (current and total)
const hundredElements = screen.getAllByText('100');
@@ -138,7 +142,7 @@ describe('AlertBox', () => {
},
};
- render( );
+ renderWithIntl( );
expect(screen.getByText('1.5k')).toBeInTheDocument();
expect(screen.getByText('15%')).toBeInTheDocument();
@@ -155,7 +159,7 @@ describe('AlertBox', () => {
},
};
- render( );
+ renderWithIntl( );
expect(screen.getByText('150')).toBeInTheDocument();
expect(screen.getByText('150%')).toBeInTheDocument();
@@ -173,7 +177,7 @@ describe('AlertBox', () => {
onCompact: mockOnCompact,
};
- render( );
+ renderWithIntl( );
expect(screen.getByText('Compact now')).toBeInTheDocument();
});
@@ -190,7 +194,7 @@ describe('AlertBox', () => {
compactIcon: ,
};
- render( );
+ renderWithIntl( );
expect(screen.getByTestId('compact-icon')).toBeInTheDocument();
expect(screen.getByText('Compact now')).toBeInTheDocument();
@@ -207,7 +211,7 @@ describe('AlertBox', () => {
onCompact: mockOnCompact,
};
- render( );
+ renderWithIntl( );
const compactButton = screen.getByText('Compact now');
await user.click(compactButton);
@@ -226,7 +230,7 @@ describe('AlertBox', () => {
onCompact: mockOnCompact,
};
- render(
+ renderWithIntl(
@@ -248,7 +252,7 @@ describe('AlertBox', () => {
onCompact: mockOnCompact,
};
- render( );
+ renderWithIntl( );
expect(screen.queryByText('Compact now')).not.toBeInTheDocument();
});
@@ -261,7 +265,7 @@ describe('AlertBox', () => {
showCompactButton: true,
};
- render( );
+ renderWithIntl( );
expect(screen.queryByText('Compact now')).not.toBeInTheDocument();
});
@@ -280,7 +284,7 @@ describe('AlertBox', () => {
onCompact: mockOnCompact,
};
- render( );
+ renderWithIntl( );
expect(screen.getByText('75')).toBeInTheDocument();
expect(screen.getByText('75%')).toBeInTheDocument();
@@ -294,7 +298,7 @@ describe('AlertBox', () => {
message: 'Line 1\nLine 2\nLine 3',
};
- render( );
+ renderWithIntl( );
// Use a function matcher to handle the whitespace-pre-line rendering
expect(
@@ -313,7 +317,7 @@ describe('AlertBox', () => {
message: '',
};
- const { container } = render( );
+ const { container } = renderWithIntl( );
// Should still render the alert container
const alertElement = container.querySelector('.flex.flex-col.gap-2');
@@ -330,7 +334,7 @@ describe('AlertBox', () => {
},
};
- render( );
+ renderWithIntl( );
expect(screen.getByText('10')).toBeInTheDocument();
expect(screen.getByText('0')).toBeInTheDocument();
diff --git a/ui/desktop/src/components/apps/AppsView.tsx b/ui/desktop/src/components/apps/AppsView.tsx
index d51b775d..526d6ddb 100644
--- a/ui/desktop/src/components/apps/AppsView.tsx
+++ b/ui/desktop/src/components/apps/AppsView.tsx
@@ -6,6 +6,52 @@ import { exportApp, GooseApp, importApp, listApps } from '../../api';
import { useChatContext } from '../../contexts/ChatContext';
import { formatAppName } from '../../utils/conversionUtils';
import { errorMessage } from '../../utils/conversionUtils';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ errorLoading: {
+ id: 'appsView.errorLoading',
+ defaultMessage: 'Error loading apps: {error}',
+ },
+ retry: {
+ id: 'appsView.retry',
+ defaultMessage: 'Retry',
+ },
+ title: {
+ id: 'appsView.title',
+ defaultMessage: 'Apps',
+ },
+ importApp: {
+ id: 'appsView.importApp',
+ defaultMessage: 'Import App',
+ },
+ description: {
+ id: 'appsView.description',
+ defaultMessage:
+ 'Applications from your MCP servers and Apps build by goose itself. You can ask it to create new apps through the chat interface and they will appear here.',
+ },
+ loading: {
+ id: 'appsView.loading',
+ defaultMessage: 'Loading apps...',
+ },
+ noAppsTitle: {
+ id: 'appsView.noAppsTitle',
+ defaultMessage: 'No apps available',
+ },
+ noAppsDescription: {
+ id: 'appsView.noAppsDescription',
+ defaultMessage:
+ 'Open a chat and ask goose for the app you want to have. It can build one for you and that will appear here. Or if somebody shared an app, you can import it using the button above.',
+ },
+ customApp: {
+ id: 'appsView.customApp',
+ defaultMessage: 'Custom app',
+ },
+ launch: {
+ id: 'appsView.launch',
+ defaultMessage: 'Launch',
+ },
+});
const GridLayout = ({ children }: { children: React.ReactNode }) => {
return (
@@ -22,6 +68,7 @@ const GridLayout = ({ children }: { children: React.ReactNode }) => {
};
export default function AppsView() {
+ const intl = useIntl();
const [apps, setApps] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
@@ -195,8 +242,8 @@ export default function AppsView() {
return (
-
Error loading apps: {error}
-
Retry
+
{intl.formatMessage(i18n.errorLoading, { error })}
+
{intl.formatMessage(i18n.retry)}
);
@@ -215,7 +262,7 @@ export default function AppsView() {
-
Apps
+ {intl.formatMessage(i18n.title)}
- Import App
+ {intl.formatMessage(i18n.importApp)}
- Applications from your MCP servers and Apps build by goose itself. You can ask it to
- create new apps through the chat interface and they will appear here.
+ {intl.formatMessage(i18n.description)}
@@ -238,16 +284,14 @@ export default function AppsView() {
{loading ? (
-
Loading apps...
+
{intl.formatMessage(i18n.loading)}
) : apps.length === 0 ? (
-
No apps available
+
{intl.formatMessage(i18n.noAppsTitle)}
- Open a chat and ask goose for the app you want to have. It can build one for you
- and that will appear here. Or if somebody shared an app, you can import it using
- the button above.
+ {intl.formatMessage(i18n.noAppsDescription)}
@@ -269,7 +313,7 @@ export default function AppsView() {
)}
{app.mcpServers && app.mcpServers.length > 0 && (
- {isCustomApp ? 'Custom app' : app.mcpServers.join(', ')}
+ {isCustomApp ? intl.formatMessage(i18n.customApp) : app.mcpServers.join(', ')}
)}
@@ -281,7 +325,7 @@ export default function AppsView() {
className="flex items-center gap-2 flex-1"
>
- Launch
+ {intl.formatMessage(i18n.launch)}
{isCustomApp && (
(null);
const [cachedHtml, setCachedHtml] = useState(null);
@@ -25,7 +42,7 @@ export default function StandaloneAppView() {
resourceUri === 'undefined' ||
extensionName === 'undefined'
) {
- setError('Missing required parameters');
+ setError(intl.formatMessage(i18n.missingParams));
setLoading(false);
return;
}
@@ -50,7 +67,7 @@ export default function StandaloneAppView() {
}
loadCachedHtml();
- }, [resourceUri, extensionName]);
+ }, [resourceUri, extensionName, intl]);
useEffect(() => {
async function initSession() {
@@ -122,7 +139,7 @@ export default function StandaloneAppView() {
padding: '24px',
}}
>
- Failed to Load App
+ {intl.formatMessage(i18n.failedToLoad)}
{error}
);
@@ -139,7 +156,7 @@ export default function StandaloneAppView() {
justifyContent: 'center',
}}
>
- Initializing app...
+ {intl.formatMessage(i18n.initializing)}
);
}
diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx
index 390db66d..e7b2758c 100644
--- a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx
+++ b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx
@@ -14,12 +14,61 @@ import {
getExtensionOverride,
getExtensionOverrides,
} from '../../store/extensionOverrides';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ manageExtensions: {
+ id: 'bottomMenuExtensionSelection.manageExtensions',
+ defaultMessage: 'manage extensions',
+ },
+ searchExtensions: {
+ id: 'bottomMenuExtensionSelection.searchExtensions',
+ defaultMessage: 'search extensions...',
+ },
+ extensionsForNewChats: {
+ id: 'bottomMenuExtensionSelection.extensionsForNewChats',
+ defaultMessage: 'Extensions for new chats',
+ },
+ extensionsForThisSession: {
+ id: 'bottomMenuExtensionSelection.extensionsForThisSession',
+ defaultMessage: 'Extensions for this chat session',
+ },
+ noExtensionsFound: {
+ id: 'bottomMenuExtensionSelection.noExtensionsFound',
+ defaultMessage: 'no extensions found',
+ },
+ noExtensionsAvailable: {
+ id: 'bottomMenuExtensionSelection.noExtensionsAvailable',
+ defaultMessage: 'no extensions available',
+ },
+ extensionUpdated: {
+ id: 'bottomMenuExtensionSelection.extensionUpdated',
+ defaultMessage: 'Extension Updated',
+ },
+ extensionWillBeEnabled: {
+ id: 'bottomMenuExtensionSelection.extensionWillBeEnabled',
+ defaultMessage: '{name} will be enabled in new chats',
+ },
+ extensionWillBeDisabled: {
+ id: 'bottomMenuExtensionSelection.extensionWillBeDisabled',
+ defaultMessage: '{name} will be disabled in new chats',
+ },
+ extensionToggleError: {
+ id: 'bottomMenuExtensionSelection.extensionToggleError',
+ defaultMessage: 'Extension Toggle Error',
+ },
+ noActiveSession: {
+ id: 'bottomMenuExtensionSelection.noActiveSession',
+ defaultMessage: 'No active session found. Please start a chat session first.',
+ },
+});
interface BottomMenuExtensionSelectionProps {
sessionId: string | null;
}
export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionSelectionProps) => {
+ const intl = useIntl();
const [searchQuery, setSearchQuery] = useState('');
const [isOpen, setIsOpen] = useState(false);
const [sessionExtensions, setSessionExtensions] = useState([]);
@@ -113,8 +162,11 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
}, 800);
toastService.success({
- title: 'Extension Updated',
- msg: `${formatExtensionName(extensionConfig.name)} will be ${!currentState ? 'enabled' : 'disabled'} in new chats`,
+ title: intl.formatMessage(i18n.extensionUpdated),
+ msg: intl.formatMessage(
+ !currentState ? i18n.extensionWillBeEnabled : i18n.extensionWillBeDisabled,
+ { name: formatExtensionName(extensionConfig.name) }
+ ),
});
return;
}
@@ -123,8 +175,8 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
setIsTransitioning(false);
setTogglingExtension(null);
toastService.error({
- title: 'Extension Toggle Error',
- msg: 'No active session found. Please start a chat session first.',
+ title: intl.formatMessage(i18n.extensionToggleError),
+ msg: intl.formatMessage(i18n.noActiveSession),
traceback: 'No session ID available',
});
return;
@@ -161,7 +213,7 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
setTogglingExtension(null);
}
},
- [sessionId, isHubView, togglingExtension]
+ [sessionId, isHubView, togglingExtension, intl]
);
// Merge all available extensions with session-specific or hub override state
@@ -233,7 +285,7 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
{activeCount}
@@ -250,14 +302,14 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
{sortedExtensions.length === 0 ? (
- {searchQuery ? 'no extensions found' : 'no extensions available'}
+ {intl.formatMessage(searchQuery ? i18n.noExtensionsFound : i18n.noExtensionsAvailable)}
) : (
sortedExtensions.map((ext) => {
diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.test.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.test.tsx
index 62964a70..67eeafbf 100644
--- a/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.test.tsx
+++ b/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.test.tsx
@@ -1,6 +1,10 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, screen, waitFor, fireEvent } from '@testing-library/react';
+import { render, type RenderOptions, screen, waitFor, fireEvent } from '@testing-library/react';
import { BottomMenuModeSelection } from './BottomMenuModeSelection';
+import { IntlTestWrapper } from '../../i18n/test-utils';
+
+const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) =>
+ render(ui, { wrapper: IntlTestWrapper, ...options });
let mockConfig: Record
= {};
const mockUpdateSession = vi.fn().mockResolvedValue({});
@@ -37,7 +41,7 @@ describe('BottomMenuModeSelection', () => {
it('displays mode from config when no session', async () => {
mockConfig.GOOSE_MODE = 'approve';
- render( );
+ renderWithIntl( );
await waitFor(() => {
expect(screen.getByText('manual')).toBeInTheDocument();
});
@@ -45,7 +49,7 @@ describe('BottomMenuModeSelection', () => {
it('defaults to auto when config has no mode', async () => {
mockConfig.GOOSE_MODE = undefined;
- render( );
+ renderWithIntl( );
await waitFor(() => {
expect(screen.getByText('autonomous')).toBeInTheDocument();
});
@@ -54,7 +58,7 @@ describe('BottomMenuModeSelection', () => {
it('fetches mode from session when sessionId is present', async () => {
mockConfig.GOOSE_MODE = 'auto';
mockGetSession.mockResolvedValue({ data: { goose_mode: 'approve' } });
- render( );
+ renderWithIntl( );
await waitFor(() => {
expect(screen.getByText('manual')).toBeInTheDocument();
});
@@ -65,7 +69,7 @@ describe('BottomMenuModeSelection', () => {
it('calls updateSession and does not write global config', async () => {
mockConfig.GOOSE_MODE = 'auto';
- render( );
+ renderWithIntl( );
fireEvent.click(screen.getByText('Manual'));
@@ -78,7 +82,7 @@ describe('BottomMenuModeSelection', () => {
it('does not call updateSession when sessionId is null', async () => {
mockConfig.GOOSE_MODE = 'auto';
- render( );
+ renderWithIntl( );
fireEvent.click(screen.getByText('Manual'));
@@ -98,7 +102,7 @@ describe('BottomMenuModeSelection', () => {
.mockImplementationOnce(() => promiseA)
.mockResolvedValueOnce({ data: { goose_mode: 'auto' } });
- const { rerender } = render( );
+ const { rerender } = renderWithIntl( );
rerender( );
await waitFor(() => {
diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.tsx
index 361c5747..ae7a6f7c 100644
--- a/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.tsx
+++ b/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.tsx
@@ -10,8 +10,25 @@ import {
} from '../ui/dropdown-menu';
import { trackModeChanged } from '../../utils/analytics';
import { getSession, updateSession } from '../../api';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ autoFallback: {
+ id: 'bottomMenuModeSelection.autoFallback',
+ defaultMessage: 'auto',
+ },
+ automaticModeDescription: {
+ id: 'bottomMenuModeSelection.automaticModeDescription',
+ defaultMessage: 'Automatic mode selection',
+ },
+ currentModeTitle: {
+ id: 'bottomMenuModeSelection.currentModeTitle',
+ defaultMessage: 'Current mode: {label} - {description}',
+ },
+});
export const BottomMenuModeSelection = ({ sessionId }: { sessionId: string | null }) => {
+ const intl = useIntl();
const [gooseMode, setGooseMode] = useState('auto');
const { config } = useConfig();
@@ -51,18 +68,20 @@ export const BottomMenuModeSelection = ({ sessionId }: { sessionId: string | nul
}
};
- function getValueByKey(key: string) {
+ function getValueByKey(key: string): string {
const mode = all_goose_modes.find((mode) => mode.key === key);
- return mode ? mode.label : 'auto';
+ if (!mode) return intl.formatMessage(i18n.autoFallback);
+ return intl.formatMessage(mode.labelDescriptor);
}
- function getModeDescription(key: string) {
+ function getModeDescription(key: string): string {
const mode = all_goose_modes.find((mode) => mode.key === key);
- return mode ? mode.description : 'Automatic mode selection';
+ if (!mode) return intl.formatMessage(i18n.automaticModeDescription);
+ return intl.formatMessage(mode.descriptionDescriptor);
}
return (
-
+
diff --git a/ui/desktop/src/components/bottom_menu/CostTracker.tsx b/ui/desktop/src/components/bottom_menu/CostTracker.tsx
index 053f57d6..e64ab96f 100644
--- a/ui/desktop/src/components/bottom_menu/CostTracker.tsx
+++ b/ui/desktop/src/components/bottom_menu/CostTracker.tsx
@@ -3,6 +3,30 @@ import { CoinIcon } from '../icons';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip';
import { fetchCanonicalModelInfo } from '../../utils/canonical';
import type { ModelInfoData } from '../../api';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ pricingUnavailable: {
+ id: 'costTracker.pricingUnavailable',
+ defaultMessage: 'Pricing data unavailable for {model}',
+ },
+ costUnavailable: {
+ id: 'costTracker.costUnavailable',
+ defaultMessage: 'Cost data not available for {model} ({inputTokens} input, {outputTokens} output tokens)',
+ },
+ sessionCostBreakdown: {
+ id: 'costTracker.sessionCostBreakdown',
+ defaultMessage: 'Session cost breakdown:',
+ },
+ totalSessionCost: {
+ id: 'costTracker.totalSessionCost',
+ defaultMessage: 'Total session cost: {cost}',
+ },
+ inputOutputTooltip: {
+ id: 'costTracker.inputOutputTooltip',
+ defaultMessage: 'Input: {inputTokens} tokens ({inputCost}) | Output: {outputTokens} tokens ({outputCost})',
+ },
+});
interface CostTrackerProps {
inputTokens?: number;
@@ -25,6 +49,7 @@ export function CostTracker({
model: currentModel,
provider: currentProvider,
}: CostTrackerProps) {
+ const intl = useIntl();
const [costInfo, setCostInfo] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [showPricing, setShowPricing] = useState(true);
@@ -162,9 +187,13 @@ export function CostTracker({
// Otherwise show as unavailable
const getUnavailableTooltip = () => {
if (pricingFailed) {
- return `Pricing data unavailable for ${currentModel}`;
+ return intl.formatMessage(i18n.pricingUnavailable, { model: currentModel });
}
- return `Cost data not available for ${currentModel} (${inputTokens.toLocaleString()} input, ${outputTokens.toLocaleString()} output tokens)`;
+ return intl.formatMessage(i18n.costUnavailable, {
+ model: currentModel,
+ inputTokens: inputTokens.toLocaleString(),
+ outputTokens: outputTokens.toLocaleString(),
+ });
};
return (
@@ -189,13 +218,13 @@ export function CostTracker({
const getTooltipContent = (): string => {
// Handle error states first
if (pricingFailed) {
- return `Pricing data unavailable for ${currentProvider}/${currentModel}`;
+ return intl.formatMessage(i18n.pricingUnavailable, { model: `${currentProvider}/${currentModel}` });
}
// Handle session costs
if (sessionCosts && Object.keys(sessionCosts).length > 0) {
// Show session breakdown
- let tooltip = 'Session cost breakdown:\n';
+ let tooltip = intl.formatMessage(i18n.sessionCostBreakdown) + '\n';
Object.entries(sessionCosts).forEach(([modelKey, cost]) => {
const costStr = `${costInfo?.currency || '$'}${cost.totalCost.toFixed(6)}`;
@@ -213,12 +242,20 @@ export function CostTracker({
}
}
- tooltip += `\nTotal session cost: ${costInfo?.currency || '$'}${totalCost.toFixed(6)}`;
+ tooltip += '\n' + intl.formatMessage(i18n.totalSessionCost, { cost: `${costInfo?.currency || '$'}${totalCost.toFixed(6)}` });
return tooltip;
}
// Default tooltip for single model
- return `Input: ${inputTokens.toLocaleString()} tokens (${costInfo?.currency || '$'}${((inputTokens * (costInfo?.input_token_cost || 0)) / 1_000_000).toFixed(6)}) | Output: ${outputTokens.toLocaleString()} tokens (${costInfo?.currency || '$'}${((outputTokens * (costInfo?.output_token_cost || 0)) / 1_000_000).toFixed(6)})`;
+ const currency = costInfo?.currency || '$';
+ const inputCostStr = `${currency}${((inputTokens * (costInfo?.input_token_cost || 0)) / 1_000_000).toFixed(6)}`;
+ const outputCostStr = `${currency}${((outputTokens * (costInfo?.output_token_cost || 0)) / 1_000_000).toFixed(6)}`;
+ return intl.formatMessage(i18n.inputOutputTooltip, {
+ inputTokens: inputTokens.toLocaleString(),
+ inputCost: inputCostStr,
+ outputTokens: outputTokens.toLocaleString(),
+ outputCost: outputCostStr,
+ });
};
return (
diff --git a/ui/desktop/src/components/bottom_menu/DirSwitcher.tsx b/ui/desktop/src/components/bottom_menu/DirSwitcher.tsx
index 5269f11e..7fb4078d 100644
--- a/ui/desktop/src/components/bottom_menu/DirSwitcher.tsx
+++ b/ui/desktop/src/components/bottom_menu/DirSwitcher.tsx
@@ -3,6 +3,14 @@ import { FolderDot } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/Tooltip';
import { updateWorkingDir } from '../../api';
import { toast } from 'react-toastify';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ failedToUpdateWorkingDir: {
+ id: 'dirSwitcher.failedToUpdateWorkingDir',
+ defaultMessage: 'Failed to update working directory',
+ },
+});
interface DirSwitcherProps {
className: string;
@@ -21,6 +29,7 @@ export const DirSwitcher: React.FC = ({
onRestartStart,
onRestartEnd,
}) => {
+ const intl = useIntl();
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
const [isDirectoryChooserOpen, setIsDirectoryChooserOpen] = useState(false);
@@ -53,7 +62,7 @@ export const DirSwitcher: React.FC = ({
});
} catch (error) {
console.error('[DirSwitcher] Failed to update working directory:', error);
- toast.error('Failed to update working directory');
+ toast.error(intl.formatMessage(i18n.failedToUpdateWorkingDir));
} finally {
onRestartEnd?.();
}
diff --git a/ui/desktop/src/components/common/Greeting.tsx b/ui/desktop/src/components/common/Greeting.tsx
index 704d2af8..66bc9963 100644
--- a/ui/desktop/src/components/common/Greeting.tsx
+++ b/ui/desktop/src/components/common/Greeting.tsx
@@ -1,5 +1,85 @@
import { useState } from 'react';
import { useTextAnimator } from '../../hooks/use-text-animator';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ readyToGetStarted: {
+ id: 'greeting.readyToGetStarted',
+ defaultMessage: 'Ready to get started?',
+ },
+ whatToWorkOn: {
+ id: 'greeting.whatToWorkOn',
+ defaultMessage: 'What would you like to work on?',
+ },
+ readyToBuild: {
+ id: 'greeting.readyToBuild',
+ defaultMessage: 'Ready to build something amazing?',
+ },
+ whatToExplore: {
+ id: 'greeting.whatToExplore',
+ defaultMessage: 'What would you like to explore?',
+ },
+ whatsOnYourMind: {
+ id: 'greeting.whatsOnYourMind',
+ defaultMessage: "What's on your mind?",
+ },
+ whatShallWeCreate: {
+ id: 'greeting.whatShallWeCreate',
+ defaultMessage: 'What shall we create today?',
+ },
+ whatProjectNeedsAttention: {
+ id: 'greeting.whatProjectNeedsAttention',
+ defaultMessage: 'What project needs attention?',
+ },
+ whatToTackle: {
+ id: 'greeting.whatToTackle',
+ defaultMessage: 'What would you like to tackle?',
+ },
+ whatNeedsToBeDone: {
+ id: 'greeting.whatNeedsToBeDone',
+ defaultMessage: 'What needs to be done?',
+ },
+ whatsThePlan: {
+ id: 'greeting.whatsThePlan',
+ defaultMessage: "What's the plan for today?",
+ },
+ readyToCreateGreat: {
+ id: 'greeting.readyToCreateGreat',
+ defaultMessage: 'Ready to create something great?',
+ },
+ whatCanBeBuilt: {
+ id: 'greeting.whatCanBeBuilt',
+ defaultMessage: 'What can be built today?',
+ },
+ whatsNextChallenge: {
+ id: 'greeting.whatsNextChallenge',
+ defaultMessage: "What's the next challenge?",
+ },
+ whatProgress: {
+ id: 'greeting.whatProgress',
+ defaultMessage: 'What progress can be made?',
+ },
+ whatToAccomplish: {
+ id: 'greeting.whatToAccomplish',
+ defaultMessage: 'What would you like to accomplish?',
+ },
+ whatTaskAwaits: {
+ id: 'greeting.whatTaskAwaits',
+ defaultMessage: 'What task awaits?',
+ },
+ whatsTheMission: {
+ id: 'greeting.whatsTheMission',
+ defaultMessage: "What's the mission today?",
+ },
+ whatCanBeAchieved: {
+ id: 'greeting.whatCanBeAchieved',
+ defaultMessage: 'What can be achieved?',
+ },
+ whatProjectReadyToBegin: {
+ id: 'greeting.whatProjectReadyToBegin',
+ defaultMessage: 'What project is ready to begin?',
+ },
+});
interface GreetingProps {
className?: string;
@@ -10,47 +90,43 @@ export function Greeting({
className = 'mt-1 text-4xl font-light animate-in fade-in duration-300',
forceRefresh = false,
}: GreetingProps) {
- const prefixes = ['Hello!'];
- const messages = [
- ' Ready to get started?',
- ' What would you like to work on?',
- ' Ready to build something amazing?',
- ' What would you like to explore?',
- " What's on your mind?",
- ' What shall we create today?',
- ' What project needs attention?',
- ' What would you like to tackle?',
- ' What would you like to explore?',
- ' What needs to be done?',
- " What's the plan for today?",
- ' Ready to create something great?',
- ' What can be built today?',
- " What's the next challenge?",
- ' What progress can be made?',
- ' What would you like to accomplish?',
- ' What task awaits?',
- " What's the mission today?",
- ' What can be achieved?',
- ' What project is ready to begin?',
+ const intl = useIntl();
+
+ const messageDescriptors = [
+ i18n.readyToGetStarted,
+ i18n.whatToWorkOn,
+ i18n.readyToBuild,
+ i18n.whatToExplore,
+ i18n.whatsOnYourMind,
+ i18n.whatShallWeCreate,
+ i18n.whatProjectNeedsAttention,
+ i18n.whatToTackle,
+ i18n.whatToExplore,
+ i18n.whatNeedsToBeDone,
+ i18n.whatsThePlan,
+ i18n.readyToCreateGreat,
+ i18n.whatCanBeBuilt,
+ i18n.whatsNextChallenge,
+ i18n.whatProgress,
+ i18n.whatToAccomplish,
+ i18n.whatTaskAwaits,
+ i18n.whatsTheMission,
+ i18n.whatCanBeAchieved,
+ i18n.whatProjectReadyToBegin,
];
// Using lazy initializer to generate random greeting on each component instance
const greeting = useState(() => {
- const randomPrefixIndex = Math.floor(Math.random() * prefixes.length);
- const randomMessageIndex = Math.floor(Math.random() * messages.length);
-
- return {
- prefix: prefixes[randomPrefixIndex],
- message: messages[randomMessageIndex],
- };
+ const randomMessageIndex = Math.floor(Math.random() * messageDescriptors.length);
+ return messageDescriptors[randomMessageIndex];
})[0];
- const messageRef = useTextAnimator({ text: greeting.message });
+ const greetingText = intl.formatMessage(greeting);
+ const messageRef = useTextAnimator({ text: greetingText });
return (
- {/* {greeting.prefix} */}
- {greeting.message}
+ {greetingText}
);
}
diff --git a/ui/desktop/src/components/common/InlineEditText.tsx b/ui/desktop/src/components/common/InlineEditText.tsx
index 5dc7cb30..8f88ac2e 100644
--- a/ui/desktop/src/components/common/InlineEditText.tsx
+++ b/ui/desktop/src/components/common/InlineEditText.tsx
@@ -1,6 +1,26 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { toast } from 'react-toastify';
import { errorMessage } from '../../utils/conversionUtils';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ enterText: {
+ id: 'inlineEditText.enterText',
+ defaultMessage: 'Enter text',
+ },
+ failedToSave: {
+ id: 'inlineEditText.failedToSave',
+ defaultMessage: 'Failed to save',
+ },
+ clickToEdit: {
+ id: 'inlineEditText.clickToEdit',
+ defaultMessage: 'Click to edit',
+ },
+ doubleClickToEdit: {
+ id: 'inlineEditText.doubleClickToEdit',
+ defaultMessage: 'Double-click to edit',
+ },
+});
interface InlineEditTextProps {
value: string;
@@ -20,7 +40,7 @@ export const InlineEditText: React.FC = ({
value,
onSave,
maxLength = 200,
- placeholder = 'Enter text',
+ placeholder,
disabled = false,
className = '',
editClassName = '',
@@ -29,6 +49,8 @@ export const InlineEditText: React.FC = ({
allowEmpty = false,
singleClickEdit = true,
}) => {
+ const intl = useIntl();
+ const resolvedPlaceholder = placeholder ?? intl.formatMessage(i18n.enterText);
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(value);
const [isSaving, setIsSaving] = useState(false);
@@ -86,7 +108,7 @@ export const InlineEditText: React.FC = ({
setIsEditing(false);
onEditEnd?.();
} catch (error) {
- const errMsg = errorMessage(error, 'Failed to save');
+ const errMsg = errorMessage(error, intl.formatMessage(i18n.failedToSave));
console.error('InlineEditText save error:', errMsg);
toast.error(errMsg);
setEditValue(originalValue.current);
@@ -94,7 +116,7 @@ export const InlineEditText: React.FC = ({
} finally {
setIsSaving(false);
}
- }, [editValue, isSaving, allowEmpty, onSave, handleCancel, onEditEnd]);
+ }, [editValue, isSaving, allowEmpty, onSave, handleCancel, onEditEnd, intl]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -149,7 +171,7 @@ export const InlineEditText: React.FC = ({
onKeyDown={handleKeyDown}
onBlur={handleBlur}
maxLength={maxLength}
- placeholder={placeholder}
+ placeholder={resolvedPlaceholder}
disabled={isSaving}
className={`
w-full px-2 py-1 border rounded
@@ -175,9 +197,9 @@ export const InlineEditText: React.FC = ({
`}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
- title={disabled ? '' : singleClickEdit ? 'Click to edit' : 'Double-click to edit'}
+ title={disabled ? '' : singleClickEdit ? intl.formatMessage(i18n.clickToEdit) : intl.formatMessage(i18n.doubleClickToEdit)}
>
- {value || {placeholder} }
+ {value || {resolvedPlaceholder} }
);
};
diff --git a/ui/desktop/src/components/context_management/CreditsExhaustedNotification.tsx b/ui/desktop/src/components/context_management/CreditsExhaustedNotification.tsx
index 2f00b431..45c83b64 100644
--- a/ui/desktop/src/components/context_management/CreditsExhaustedNotification.tsx
+++ b/ui/desktop/src/components/context_management/CreditsExhaustedNotification.tsx
@@ -2,6 +2,18 @@ import React from 'react';
import { AlertTriangle, ExternalLink } from 'lucide-react';
import { Message, SystemNotificationContent } from '../../api';
import { WEB_PROTOCOLS } from '../../utils/urlSecurity';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ insufficientCredits: {
+ id: 'creditsExhaustedNotification.insufficientCredits',
+ defaultMessage: 'Insufficient Credits',
+ },
+ addCredits: {
+ id: 'creditsExhaustedNotification.addCredits',
+ defaultMessage: 'Add credits',
+ },
+});
interface CreditsExhaustedNotificationProps {
notification: SystemNotificationContent;
@@ -36,6 +48,7 @@ function getValidatedTopUpUrl(data: unknown): string | null {
export const CreditsExhaustedNotification: React.FC
= ({
notification,
}) => {
+ const intl = useIntl();
const topUpUrl = getValidatedTopUpUrl(notification.data);
const handleTopUp = () => {
@@ -50,7 +63,7 @@ export const CreditsExhaustedNotification: React.FC
- Insufficient Credits
+ {intl.formatMessage(i18n.insufficientCredits)}
{notification.msg}
@@ -60,7 +73,7 @@ export const CreditsExhaustedNotification: React.FC
- Add credits
+ {intl.formatMessage(i18n.addCredits)}
)}
diff --git a/ui/desktop/src/components/conversation/SearchBar.tsx b/ui/desktop/src/components/conversation/SearchBar.tsx
index f4088455..35c2ad7e 100644
--- a/ui/desktop/src/components/conversation/SearchBar.tsx
+++ b/ui/desktop/src/components/conversation/SearchBar.tsx
@@ -1,9 +1,33 @@
import React, { useEffect, useState, useRef, KeyboardEvent } from 'react';
+import { defineMessages, useIntl } from '../../i18n';
import { Search as SearchIcon } from 'lucide-react';
import { ArrowDown, ArrowUp, Close } from '../icons';
import debounce from 'lodash/debounce';
import { Button } from '../ui/button';
+const i18nMessages = defineMessages({
+ defaultPlaceholder: {
+ id: 'searchBar.placeholder',
+ defaultMessage: 'Search conversation...',
+ },
+ caseSensitive: {
+ id: 'searchBar.caseSensitive',
+ defaultMessage: 'Case Sensitive',
+ },
+ previous: {
+ id: 'searchBar.previous',
+ defaultMessage: 'Previous ({shortcut})',
+ },
+ next: {
+ id: 'searchBar.next',
+ defaultMessage: 'Next ({shortcut})',
+ },
+ close: {
+ id: 'searchBar.close',
+ defaultMessage: 'Close ({shortcut})',
+ },
+});
+
/**
* Props for the SearchBar component
*/
@@ -37,8 +61,10 @@ export const SearchBar: React.FC = ({
searchResults,
inputRef: externalInputRef,
initialSearchTerm = '',
- placeholder = 'Search conversation...',
+ placeholder,
}: SearchBarProps) => {
+ const intl = useIntl();
+ const resolvedPlaceholder = placeholder ?? intl.formatMessage(i18nMessages.defaultPlaceholder);
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
const [caseSensitive, setCaseSensitive] = useState(false);
const [isExiting, setIsExiting] = useState(false);
@@ -161,7 +187,7 @@ export const SearchBar: React.FC = ({
value={searchTerm}
onChange={handleSearch}
onKeyDown={handleKeyDown}
- placeholder={placeholder}
+ placeholder={resolvedPlaceholder}
className="no-drag w-full text-sm pl-9 pr-24 py-3 bg-background-inverse text-text-inverse
placeholder:text-text-inverse/50 focus:outline-none
active:border-border-secondary"
@@ -190,7 +216,7 @@ export const SearchBar: React.FC = ({
? 'bg-white/20 shadow-[inset_0_1px_2px_rgba(0,0,0,0.2)] text-text-inverse hover:bg-white/25'
: 'text-text-inverse/70 hover:text-text-inverse hover:bg-white/10'
}`}
- title="Case Sensitive"
+ title={intl.formatMessage(i18nMessages.caseSensitive)}
>
Aa
@@ -200,7 +226,7 @@ export const SearchBar: React.FC = ({
onClick={(e) => handleNavigate('prev', e)}
variant="ghost"
className="no-drag flex items-center justify-center min-w-[32px] h-[28px] rounded transition-all duration-150 text-text-inverse/70 hover:text-text-inverse hover:bg-white/10"
- title="Previous (↑)"
+ title={intl.formatMessage(i18nMessages.previous, { shortcut: '↑' })}
>
= ({
onClick={(e) => handleNavigate('next', e)}
variant="ghost"
className="no-drag flex items-center justify-center min-w-[32px] h-[28px] rounded transition-all duration-150 text-text-inverse/70 hover:text-text-inverse hover:bg-white/10"
- title="Next (↓ or Enter)"
+ title={intl.formatMessage(i18nMessages.next, { shortcut: '↓ or Enter' })}
>
= ({
onClick={handleClose}
variant="ghost"
className="no-drag flex items-center justify-center min-w-[32px] h-[28px] rounded transition-all duration-150 text-text-inverse/70 hover:text-text-inverse hover:bg-white/10"
- title="Close (Esc)"
+ title={intl.formatMessage(i18nMessages.close, { shortcut: 'Esc' })}
>
diff --git a/ui/desktop/src/components/extensions/ExtensionsView.tsx b/ui/desktop/src/components/extensions/ExtensionsView.tsx
index d7ccddba..b0dcaf15 100644
--- a/ui/desktop/src/components/extensions/ExtensionsView.tsx
+++ b/ui/desktop/src/components/extensions/ExtensionsView.tsx
@@ -17,6 +17,40 @@ import { activateExtensionDefault } from '../settings/extensions';
import { useConfig } from '../ConfigContext';
import { SearchView } from '../conversation/SearchView';
import { getSearchShortcutText } from '../../utils/keyboardShortcuts';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ heading: {
+ id: 'extensionsView.heading',
+ defaultMessage: 'Extensions',
+ },
+ description: {
+ id: 'extensionsView.description',
+ defaultMessage:
+ 'These extensions use the Model Context Protocol (MCP). They can expand Goose\'s capabilities using three main components: Prompts, Resources, and Tools. {searchShortcut} to search.',
+ },
+ defaultNote: {
+ id: 'extensionsView.defaultNote',
+ defaultMessage:
+ 'Extensions enabled here are used as the default for new chats. You can also toggle active extensions during chat.',
+ },
+ addCustomExtension: {
+ id: 'extensionsView.addCustomExtension',
+ defaultMessage: 'Add custom extension',
+ },
+ browseExtensions: {
+ id: 'extensionsView.browseExtensions',
+ defaultMessage: 'Browse extensions',
+ },
+ searchPlaceholder: {
+ id: 'extensionsView.searchPlaceholder',
+ defaultMessage: 'Search extensions...',
+ },
+ addExtension: {
+ id: 'extensionsView.addExtension',
+ defaultMessage: 'Add Extension',
+ },
+});
export type ExtensionsViewOptions = {
deepLinkConfig?: ExtensionConfig;
@@ -30,6 +64,7 @@ export default function ExtensionsView({
setView: (view: View, viewOptions?: ViewOptions) => void;
viewOptions: ExtensionsViewOptions;
}) {
+ const intl = useIntl();
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const [searchTerm, setSearchTerm] = useState('');
@@ -98,16 +133,13 @@ export default function ExtensionsView({
-
Extensions
+ {intl.formatMessage(i18n.heading)}
- These extensions use the Model Context Protocol (MCP). They can expand Goose's
- capabilities using three main components: Prompts, Resources, and Tools.{' '}
- {getSearchShortcutText()} to search.
+ {intl.formatMessage(i18n.description, { searchShortcut: getSearchShortcutText() })}
- Extensions enabled here are used as the default for new chats. You can also toggle
- active extensions during chat.
+ {intl.formatMessage(i18n.defaultNote)}
{/* Action Buttons */}
@@ -118,7 +150,7 @@ export default function ExtensionsView({
onClick={() => setIsAddModalOpen(true)}
>
- Add custom extension
+ {intl.formatMessage(i18n.addCustomExtension)}
- Browse extensions
+ {intl.formatMessage(i18n.browseExtensions)}
-
setSearchTerm(term)} placeholder="Search extensions...">
+ setSearchTerm(term)} placeholder={intl.formatMessage(i18n.searchPlaceholder)}>
)}
diff --git a/ui/desktop/src/components/onboarding/FreeOptionCards.tsx b/ui/desktop/src/components/onboarding/FreeOptionCards.tsx
index 2851a8b6..4a1f338d 100644
--- a/ui/desktop/src/components/onboarding/FreeOptionCards.tsx
+++ b/ui/desktop/src/components/onboarding/FreeOptionCards.tsx
@@ -5,6 +5,50 @@ import { Tetrate } from '../icons';
import LocalModelPicker from './LocalModelPicker';
import { HardDrive } from 'lucide-react';
import { useFeatures } from '../../contexts/FeaturesContext';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ chooseOption: {
+ id: 'freeOptionCards.chooseOption',
+ defaultMessage: 'Choose an option to get started.',
+ },
+ tetrateTitle: {
+ id: 'freeOptionCards.tetrateTitle',
+ defaultMessage: 'Agent Router by Tetrate',
+ },
+ tetrateDescription: {
+ id: 'freeOptionCards.tetrateDescription',
+ defaultMessage: 'Access multiple AI models with automatic setup. Sign up to receive $10 credit.',
+ },
+ nanogptTitle: {
+ id: 'freeOptionCards.nanogptTitle',
+ defaultMessage: 'NanoGPT',
+ },
+ nanogptDescription: {
+ id: 'freeOptionCards.nanogptDescription',
+ defaultMessage: 'Sign up to receive 60M free tokens for 7 days.',
+ },
+ localModelTitle: {
+ id: 'freeOptionCards.localModelTitle',
+ defaultMessage: 'Use a Local Model',
+ },
+ freeAndPrivate: {
+ id: 'freeOptionCards.freeAndPrivate',
+ defaultMessage: 'Free & Private',
+ },
+ localModelDescription: {
+ id: 'freeOptionCards.localModelDescription',
+ defaultMessage: 'Download a model and run entirely on your machine. No API keys, no accounts.',
+ },
+ unexpectedError: {
+ id: 'freeOptionCards.unexpectedError',
+ defaultMessage: 'An unexpected error occurred during setup.',
+ },
+ retry: {
+ id: 'freeOptionCards.retry',
+ defaultMessage: 'Retry',
+ },
+});
const TETRATE = 'tetrate' as const;
const NANOGPT = 'nano-gpt' as const;
@@ -27,6 +71,7 @@ const cardClass = (isSelected: boolean) =>
}`;
export default function FreeOptionCards({ onConfigured }: FreeOptionCardsProps) {
+ const intl = useIntl();
const { localInference } = useFeatures();
const [error, setError] = useState<{
message: string;
@@ -45,7 +90,7 @@ export default function FreeOptionCards({ onConfigured }: FreeOptionCardsProps)
setError({ message: result.message, type });
}
} catch {
- setError({ message: 'An unexpected error occurred during setup.', type });
+ setError({ message: intl.formatMessage(i18n.unexpectedError), type });
}
};
@@ -74,7 +119,7 @@ export default function FreeOptionCards({ onConfigured }: FreeOptionCardsProps)
return (
-
Choose an option to get started.
+
{intl.formatMessage(i18n.chooseOption)}
@@ -82,7 +127,7 @@ export default function FreeOptionCards({ onConfigured }: FreeOptionCardsProps)
- Agent Router by Tetrate
+ {intl.formatMessage(i18n.tetrateTitle)}
@@ -90,7 +135,7 @@ export default function FreeOptionCards({ onConfigured }: FreeOptionCardsProps)
- Access multiple AI models with automatic setup. Sign up to receive $10 credit.
+ {intl.formatMessage(i18n.tetrateDescription)}
@@ -100,14 +145,14 @@ export default function FreeOptionCards({ onConfigured }: FreeOptionCardsProps)
N
-
NanoGPT
+
{intl.formatMessage(i18n.nanogptTitle)}
- Sign up to receive 60M free tokens for 7 days.
+ {intl.formatMessage(i18n.nanogptDescription)}
@@ -116,9 +161,9 @@ export default function FreeOptionCards({ onConfigured }: FreeOptionCardsProps)
- Use a Local Model
+ {intl.formatMessage(i18n.localModelTitle)}
- Free & Private
+ {intl.formatMessage(i18n.freeAndPrivate)}
@@ -126,7 +171,7 @@ export default function FreeOptionCards({ onConfigured }: FreeOptionCardsProps)
- Download a model and run entirely on your machine. No API keys, no accounts.
+ {intl.formatMessage(i18n.localModelDescription)}
)}
@@ -139,7 +184,7 @@ export default function FreeOptionCards({ onConfigured }: FreeOptionCardsProps)
onClick={handleRetry}
className="px-3 py-1 text-sm font-medium text-red-700 dark:text-red-400 bg-white dark:bg-gray-800 border border-red-300 dark:border-red-700 rounded-md hover:bg-red-50 dark:hover:bg-red-900/30 shrink-0"
>
- Retry
+ {intl.formatMessage(i18n.retry)}
)}
diff --git a/ui/desktop/src/components/onboarding/LocalModelPicker.tsx b/ui/desktop/src/components/onboarding/LocalModelPicker.tsx
index b3492132..f16467aa 100644
--- a/ui/desktop/src/components/onboarding/LocalModelPicker.tsx
+++ b/ui/desktop/src/components/onboarding/LocalModelPicker.tsx
@@ -8,6 +8,82 @@ import {
type LocalModelResponse,
} from '../../api';
import { trackOnboardingSetupFailed } from '../../utils/analytics';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ checkingModels: {
+ id: 'localModelPicker.checkingModels',
+ defaultMessage: 'Checking available models...',
+ },
+ tryAgain: {
+ id: 'localModelPicker.tryAgain',
+ defaultMessage: 'Try Again',
+ },
+ bestForMachine: {
+ id: 'localModelPicker.bestForMachine',
+ defaultMessage: 'Best for your machine',
+ },
+ ready: {
+ id: 'localModelPicker.ready',
+ defaultMessage: 'Ready',
+ },
+ showOtherSizes: {
+ id: 'localModelPicker.showOtherSizes',
+ defaultMessage: 'Show {count} other sizes',
+ },
+ hideOtherSizes: {
+ id: 'localModelPicker.hideOtherSizes',
+ defaultMessage: 'Hide other sizes',
+ },
+ selectModel: {
+ id: 'localModelPicker.selectModel',
+ defaultMessage: 'Select a model',
+ },
+ useModel: {
+ id: 'localModelPicker.useModel',
+ defaultMessage: 'Use {modelId}',
+ },
+ downloadModel: {
+ id: 'localModelPicker.downloadModel',
+ defaultMessage: 'Download {modelId} ({size})',
+ },
+ back: {
+ id: 'localModelPicker.back',
+ defaultMessage: 'Back',
+ },
+ downloading: {
+ id: 'localModelPicker.downloading',
+ defaultMessage: 'Downloading {modelId}',
+ },
+ startingDownload: {
+ id: 'localModelPicker.startingDownload',
+ defaultMessage: 'Starting download...',
+ },
+ cancelDownload: {
+ id: 'localModelPicker.cancelDownload',
+ defaultMessage: 'Cancel Download',
+ },
+ localModelsNote: {
+ id: 'localModelPicker.localModelsNote',
+ defaultMessage: 'Local models keep everything on your machine for full privacy. Performance and context window size may vary compared to cloud providers depending on your hardware and model size.',
+ },
+ failedToLoad: {
+ id: 'localModelPicker.failedToLoad',
+ defaultMessage: 'Failed to load available models. Please try again.',
+ },
+ modelNotFound: {
+ id: 'localModelPicker.modelNotFound',
+ defaultMessage: 'Model not found',
+ },
+ failedToStartDownload: {
+ id: 'localModelPicker.failedToStartDownload',
+ defaultMessage: 'Failed to start download. Please try again.',
+ },
+ lostConnection: {
+ id: 'localModelPicker.lostConnection',
+ defaultMessage: 'Lost connection to download. Please try again.',
+ },
+});
interface LocalModelPickerProps {
onConfigured: (providerName: string, modelId: string) => void;
@@ -31,6 +107,7 @@ const LOCAL_PROVIDER = 'local';
type Phase = 'loading' | 'select' | 'downloading' | 'error';
export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPickerProps) {
+ const intl = useIntl();
const [phase, setPhase] = useState('loading');
const [models, setModels] = useState([]);
const [selectedModelId, setSelectedModelId] = useState(null);
@@ -65,14 +142,14 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
}
} catch (error) {
console.error('Failed to load local models:', error);
- setErrorMessage('Failed to load available models. Please try again.');
+ setErrorMessage(intl.formatMessage(i18n.failedToLoad));
setPhase('error');
return;
}
setPhase('select');
};
load();
- }, []);
+ }, [intl]);
const finishSetup = (modelId: string) => {
onConfigured(LOCAL_PROVIDER, modelId);
@@ -85,7 +162,7 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
const model = models.find((m) => m.id === modelId);
if (!model) {
- setErrorMessage('Model not found');
+ setErrorMessage(intl.formatMessage(i18n.modelNotFound));
setPhase('error');
return;
}
@@ -94,7 +171,7 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
await downloadHfModel({ body: { spec: model.id }, throwOnError: true });
} catch (error) {
console.error('Failed to start download:', error);
- setErrorMessage('Failed to start download. Please try again.');
+ setErrorMessage(intl.formatMessage(i18n.failedToStartDownload));
trackOnboardingSetupFailed(LOCAL_PROVIDER, 'download_start_failed');
setPhase('error');
return;
@@ -123,7 +200,7 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
}
} catch {
cleanup();
- setErrorMessage('Lost connection to download. Please try again.');
+ setErrorMessage(intl.formatMessage(i18n.lostConnection));
trackOnboardingSetupFailed(LOCAL_PROVIDER, 'progress_poll_failed');
setPhase('error');
}
@@ -162,7 +239,7 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
return (
-
Checking available models...
+
{intl.formatMessage(i18n.checkingModels)}
);
}
@@ -182,7 +259,7 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
}}
className="w-full px-4 py-2 bg-transparent border rounded-lg text-text-default text-sm font-medium hover:bg-background-muted/80 transition-colors"
>
- Try Again
+ {intl.formatMessage(i18n.tryAgain)}
)}
@@ -200,7 +277,7 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
>
- Best for your machine
+ {intl.formatMessage(i18n.bestForMachine)}
@@ -217,7 +294,7 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
{recommended.status.state === 'Downloaded' && (
- Ready
+ {intl.formatMessage(i18n.ready)}
)}
@@ -235,7 +312,7 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
onClick={() => setShowAllModels(!showAllModels)}
className="text-sm text-blue-500 hover:text-blue-400 transition-colors flex items-center gap-1"
>
- {showAllModels ? 'Hide other sizes' : `Show ${otherModels.length} other sizes`}
+ {showAllModels ? intl.formatMessage(i18n.hideOtherSizes) : intl.formatMessage(i18n.showOtherSizes, { count: otherModels.length })}
{selectedModel?.status.state === 'Downloaded'
- ? `Use ${selectedModel.id}`
+ ? intl.formatMessage(i18n.useModel, { modelId: selectedModel.id })
: selectedModel
- ? `Download ${selectedModel.id} (${formatSize(selectedModel.size_bytes)})`
- : 'Select a model'}
+ ? intl.formatMessage(i18n.downloadModel, { modelId: selectedModel.id, size: formatSize(selectedModel.size_bytes) })
+ : intl.formatMessage(i18n.selectModel)}
{onBack && (
@@ -310,7 +387,7 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
onClick={onBack}
className="w-full px-4 py-2.5 text-blue-600 dark:text-blue-400 text-sm font-medium border border-blue-300 dark:border-blue-700 rounded-lg hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors cursor-pointer"
>
- Back
+ {intl.formatMessage(i18n.back)}
)}
@@ -320,7 +397,7 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
- Downloading {selectedModel.id}
+ {intl.formatMessage(i18n.downloading, { modelId: selectedModel.id })}
{downloadProgress ? (
@@ -360,7 +437,7 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
) : (
-
Starting download...
+
{intl.formatMessage(i18n.startingDownload)}
)}
@@ -369,16 +446,14 @@ export default function LocalModelPicker({ onConfigured, onBack }: LocalModelPic
onClick={handleCancelDownload}
className="w-full px-4 py-2.5 bg-transparent text-text-muted border rounded-lg text-sm hover:bg-background-default/80 transition-colors"
>
- Cancel Download
+ {intl.formatMessage(i18n.cancelDownload)}
)}
- Local models keep everything on your machine for full privacy. Performance and context
- window size may vary compared to cloud providers depending on your hardware and model
- size.
+ {intl.formatMessage(i18n.localModelsNote)}
diff --git a/ui/desktop/src/components/onboarding/OnboardingGuard.tsx b/ui/desktop/src/components/onboarding/OnboardingGuard.tsx
index 89a44b85..8c1c8dd2 100644
--- a/ui/desktop/src/components/onboarding/OnboardingGuard.tsx
+++ b/ui/desktop/src/components/onboarding/OnboardingGuard.tsx
@@ -12,6 +12,18 @@ import {
trackTelemetryPreference,
setTelemetryEnabled as setAnalyticsTelemetryEnabled,
} from '../../utils/analytics';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ welcomeTitle: {
+ id: 'onboardingGuard.welcomeTitle',
+ defaultMessage: 'Welcome to goose',
+ },
+ welcomeDescription: {
+ id: 'onboardingGuard.welcomeDescription',
+ defaultMessage: 'Your local AI agent. Connect an AI model provider to get started.',
+ },
+});
const TELEMETRY_CONFIG_KEY = 'GOOSE_TELEMETRY_ENABLED';
@@ -20,6 +32,7 @@ interface OnboardingGuardProps {
}
export default function OnboardingGuard({ children }: OnboardingGuardProps) {
+ const intl = useIntl();
const navigate = useNavigate();
const { read, upsert, getProviders } = useConfig();
const { refreshCurrentModelAndProvider } = useModelAndProvider();
@@ -117,9 +130,9 @@ export default function OnboardingGuard({ children }: OnboardingGuardProps) {
- Welcome to goose
+ {intl.formatMessage(i18n.welcomeTitle)}
- Your local AI agent. Connect an AI model provider to get started.
+ {intl.formatMessage(i18n.welcomeDescription)}
diff --git a/ui/desktop/src/components/onboarding/OnboardingSuccess.tsx b/ui/desktop/src/components/onboarding/OnboardingSuccess.tsx
index 6ba09891..5f0b121c 100644
--- a/ui/desktop/src/components/onboarding/OnboardingSuccess.tsx
+++ b/ui/desktop/src/components/onboarding/OnboardingSuccess.tsx
@@ -1,15 +1,52 @@
import { useState } from 'react';
import { Button } from '../ui/button';
import PrivacyInfoModal from './PrivacyInfoModal';
+import { defineMessages, useIntl } from '../../i18n';
const LOCAL_PROVIDER = 'local';
+const i18n = defineMessages({
+ localModelReady: {
+ id: 'onboardingSuccess.localModelReady',
+ defaultMessage: 'Local model ready',
+ },
+ connectedTo: {
+ id: 'onboardingSuccess.connectedTo',
+ defaultMessage: 'Connected to {providerName}',
+ },
+ allSet: {
+ id: 'onboardingSuccess.allSet',
+ defaultMessage: "You're all set to start using goose.",
+ },
+ privacyTitle: {
+ id: 'onboardingSuccess.privacyTitle',
+ defaultMessage: 'Privacy',
+ },
+ privacyDescription: {
+ id: 'onboardingSuccess.privacyDescription',
+ defaultMessage: 'Anonymous usage data helps improve goose. We never collect your conversations, code, or personal data.',
+ },
+ learnMore: {
+ id: 'onboardingSuccess.learnMore',
+ defaultMessage: 'Learn more',
+ },
+ shareUsageData: {
+ id: 'onboardingSuccess.shareUsageData',
+ defaultMessage: 'Share anonymous usage data',
+ },
+ getStarted: {
+ id: 'onboardingSuccess.getStarted',
+ defaultMessage: 'Get Started',
+ },
+});
+
interface OnboardingSuccessProps {
providerName: string;
onFinish: (telemetryEnabled: boolean) => void;
}
export default function OnboardingSuccess({ providerName, onFinish }: OnboardingSuccessProps) {
+ const intl = useIntl();
const [showPrivacyInfo, setShowPrivacyInfo] = useState(false);
const [telemetryOptIn, setTelemetryOptIn] = useState(true);
@@ -36,22 +73,21 @@ export default function OnboardingSuccess({ providerName, onFinish }: Onboarding
{providerName === LOCAL_PROVIDER
- ? 'Local model ready'
- : `Connected to ${providerName}`}
+ ? intl.formatMessage(i18n.localModelReady)
+ : intl.formatMessage(i18n.connectedTo, { providerName })}
- You're all set to start using goose.
+ {intl.formatMessage(i18n.allSet)}
-
Privacy
+
{intl.formatMessage(i18n.privacyTitle)}
- Anonymous usage data helps improve goose. We never collect your conversations, code,
- or personal data.{' '}
+ {intl.formatMessage(i18n.privacyDescription)}{' '}
setShowPrivacyInfo(true)}
className="text-blue-600 dark:text-blue-400 hover:underline"
>
- Learn more
+ {intl.formatMessage(i18n.learnMore)}
@@ -61,12 +97,12 @@ export default function OnboardingSuccess({ providerName, onFinish }: Onboarding
onChange={(e) => setTelemetryOptIn(e.target.checked)}
className="rounded"
/>
- Share anonymous usage data
+ {intl.formatMessage(i18n.shareUsageData)}
onFinish(telemetryOptIn)} className="w-full">
- Get Started
+ {intl.formatMessage(i18n.getStarted)}
diff --git a/ui/desktop/src/components/onboarding/PrivacyInfoModal.tsx b/ui/desktop/src/components/onboarding/PrivacyInfoModal.tsx
index 5dd3ee20..739fd69e 100644
--- a/ui/desktop/src/components/onboarding/PrivacyInfoModal.tsx
+++ b/ui/desktop/src/components/onboarding/PrivacyInfoModal.tsx
@@ -1,4 +1,48 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'privacyInfoModal.title',
+ defaultMessage: 'Privacy details',
+ },
+ description: {
+ id: 'privacyInfoModal.description',
+ defaultMessage: 'Anonymous usage data helps us understand how goose is used and identify areas for improvement.',
+ },
+ whatWeCollect: {
+ id: 'privacyInfoModal.whatWeCollect',
+ defaultMessage: 'What we collect:',
+ },
+ collectOs: {
+ id: 'privacyInfoModal.collectOs',
+ defaultMessage: 'Operating system, version, and architecture',
+ },
+ collectVersion: {
+ id: 'privacyInfoModal.collectVersion',
+ defaultMessage: 'goose version and install method',
+ },
+ collectProvider: {
+ id: 'privacyInfoModal.collectProvider',
+ defaultMessage: 'Provider and model used',
+ },
+ collectExtensions: {
+ id: 'privacyInfoModal.collectExtensions',
+ defaultMessage: 'Extensions and tool usage counts (names only)',
+ },
+ collectSession: {
+ id: 'privacyInfoModal.collectSession',
+ defaultMessage: 'Session metrics (duration, interaction count, token usage)',
+ },
+ collectErrors: {
+ id: 'privacyInfoModal.collectErrors',
+ defaultMessage: 'Error types (e.g., "rate_limit", "auth" - no details)',
+ },
+ neverCollect: {
+ id: 'privacyInfoModal.neverCollect',
+ defaultMessage: 'We never collect your conversations, code, tool arguments, error messages, or any personal data. You can change this setting anytime in Settings.',
+ },
+});
interface PrivacyInfoModalProps {
isOpen: boolean;
@@ -6,30 +50,30 @@ interface PrivacyInfoModalProps {
}
export default function PrivacyInfoModal({ isOpen, onClose }: PrivacyInfoModalProps) {
+ const intl = useIntl();
+
return (
!open && onClose()}>
- Privacy details
+ {intl.formatMessage(i18n.title)}
- Anonymous usage data helps us understand how goose is used and identify areas for
- improvement.
+ {intl.formatMessage(i18n.description)}
-
What we collect:
+
{intl.formatMessage(i18n.whatWeCollect)}
- Operating system, version, and architecture
- goose version and install method
- Provider and model used
- Extensions and tool usage counts (names only)
- Session metrics (duration, interaction count, token usage)
- Error types (e.g., "rate_limit", "auth" - no details)
+ {intl.formatMessage(i18n.collectOs)}
+ {intl.formatMessage(i18n.collectVersion)}
+ {intl.formatMessage(i18n.collectProvider)}
+ {intl.formatMessage(i18n.collectExtensions)}
+ {intl.formatMessage(i18n.collectSession)}
+ {intl.formatMessage(i18n.collectErrors)}
- We never collect your conversations, code, tool arguments, error messages, or any
- personal data. You can change this setting anytime in Settings.
+ {intl.formatMessage(i18n.neverCollect)}
diff --git a/ui/desktop/src/components/onboarding/ProviderConfigForm.tsx b/ui/desktop/src/components/onboarding/ProviderConfigForm.tsx
index 16c90183..ebf1acd6 100644
--- a/ui/desktop/src/components/onboarding/ProviderConfigForm.tsx
+++ b/ui/desktop/src/components/onboarding/ProviderConfigForm.tsx
@@ -9,6 +9,39 @@ import ProviderLogo from '../settings/providers/modal/subcomponents/ProviderLogo
import { SecureStorageNotice } from '../settings/providers/modal/subcomponents/SecureStorageNotice';
import { Button } from '../ui/button';
import { LogIn, ChevronRight } from 'lucide-react';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ browserWindowOpen: {
+ id: 'providerConfigForm.browserWindowOpen',
+ defaultMessage: 'A browser window will open for you to complete the login.',
+ },
+ deviceCodeFlowHint: {
+ id: 'providerConfigForm.deviceCodeFlowHint',
+ defaultMessage:
+ 'A browser window will open and the verification code will be copied to your clipboard. Paste it in the browser to complete sign-in.',
+ },
+ signingIn: {
+ id: 'providerConfigForm.signingIn',
+ defaultMessage: 'Signing in...',
+ },
+ signInWith: {
+ id: 'providerConfigForm.signInWith',
+ defaultMessage: 'Sign in with {providerName}',
+ },
+ noApiKey: {
+ id: 'providerConfigForm.noApiKey',
+ defaultMessage: "Don't have an API key?",
+ },
+ configuring: {
+ id: 'providerConfigForm.configuring',
+ defaultMessage: 'Configuring...',
+ },
+ continue: {
+ id: 'providerConfigForm.continue',
+ defaultMessage: 'Continue',
+ },
+});
function parseLinks(text: string) {
return text.split(/(https?:\/\/[^\s]+)/g).map((part, i) =>
@@ -39,6 +72,7 @@ function OAuthForm({
onConfigured: (name: string) => void;
onError: (msg: string) => void;
}) {
+ const intl = useIntl();
const [isLoading, setIsLoading] = useState(false);
const handleLogin = async () => {
@@ -67,12 +101,12 @@ function OAuthForm({
size="lg"
>
- {isLoading ? 'Signing in...' : `Sign in with ${provider.metadata.display_name}`}
+ {isLoading ? intl.formatMessage(i18n.signingIn) : intl.formatMessage(i18n.signInWith, { providerName: provider.metadata.display_name })}
{isDeviceCodeFlow
- ? 'A browser window will open and the verification code will be copied to your clipboard. Paste it in the browser to complete sign-in.'
- : 'A browser window will open for you to complete the login.'}
+ ? intl.formatMessage(i18n.deviceCodeFlowHint)
+ : intl.formatMessage(i18n.browserWindowOpen)}
);
@@ -87,6 +121,7 @@ function ApiKeyForm({
onConfigured: (name: string) => void;
onError: (msg: string) => void;
}) {
+ const intl = useIntl();
const { upsert } = useConfig();
const [configValues, setConfigValues] = useState>({});
const [validationErrors, setValidationErrors] = useState>({});
@@ -159,7 +194,7 @@ function ApiKeyForm({
size={14}
className={`transition-transform duration-200 ${showSetupHelp ? 'rotate-90' : ''}`}
/>
- Don't have an API key?
+ {intl.formatMessage(i18n.noApiKey)}
{showSetupHelp && (
@@ -172,7 +207,7 @@ function ApiKeyForm({
)}
- {isSubmitting ? 'Configuring...' : 'Continue'}
+ {isSubmitting ? intl.formatMessage(i18n.configuring) : intl.formatMessage(i18n.continue)}
diff --git a/ui/desktop/src/components/onboarding/ProviderSelector.tsx b/ui/desktop/src/components/onboarding/ProviderSelector.tsx
index 06376d58..ceb58ffc 100644
--- a/ui/desktop/src/components/onboarding/ProviderSelector.tsx
+++ b/ui/desktop/src/components/onboarding/ProviderSelector.tsx
@@ -11,6 +11,38 @@ import FreeOptionCards from './FreeOptionCards';
import CustomProviderForm from '../settings/providers/modal/subcomponents/forms/CustomProviderForm';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog';
import { Gift, Key, Plus } from 'lucide-react';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ useFreeLocal: {
+ id: 'providerSelector.useFreeLocal',
+ defaultMessage: 'Use Free/Local Providers',
+ },
+ freeLocalDescription: {
+ id: 'providerSelector.freeLocalDescription',
+ defaultMessage: 'Use a local model or a provider with free credits',
+ },
+ connectProvider: {
+ id: 'providerSelector.connectProvider',
+ defaultMessage: 'Connect to a Provider',
+ },
+ connectProviderDescription: {
+ id: 'providerSelector.connectProviderDescription',
+ defaultMessage: 'Connect OpenAI, Anthropic, Google, etc',
+ },
+ selectProvider: {
+ id: 'providerSelector.selectProvider',
+ defaultMessage: 'Select a provider',
+ },
+ addCustomProvider: {
+ id: 'providerSelector.addCustomProvider',
+ defaultMessage: 'Add a custom provider',
+ },
+ addCustomProviderTitle: {
+ id: 'providerSelector.addCustomProviderTitle',
+ defaultMessage: 'Add Custom Provider',
+ },
+});
const FREE_OPTIONS = 'free-options' as const;
const OWN_PROVIDER = 'own-provider' as const;
@@ -32,6 +64,7 @@ export default function ProviderSelector({
onConfigured,
onFirstSelection,
}: ProviderSelectorProps) {
+ const intl = useIntl();
const [providerList, setProviderList] = useState([]);
const [selectedOption, setSelectedOption] = useState(null);
const [selectedPath, setSelectedPath] = useState(null);
@@ -116,10 +149,10 @@ export default function ProviderSelector({
>
- Use Free/Local Providers
+ {intl.formatMessage(i18n.useFreeLocal)}
- Use a local model or a provider with free credits
+ {intl.formatMessage(i18n.freeLocalDescription)}
@@ -133,9 +166,9 @@ export default function ProviderSelector({
>
- Connect to a Provider
+ {intl.formatMessage(i18n.connectProvider)}
- Connect OpenAI, Anthropic, Google, etc
+ {intl.formatMessage(i18n.connectProviderDescription)}
@@ -152,7 +185,7 @@ export default function ProviderSelector({
options={options}
value={selectedOption}
onChange={(option) => handleProviderSelect(option as ProviderOption | null)}
- placeholder="Select a provider"
+ placeholder={intl.formatMessage(i18n.selectProvider)}
isClearable
isSearchable
autoFocus
@@ -165,7 +198,7 @@ export default function ProviderSelector({
className="flex items-center gap-1 text-sm text-text-muted hover:text-text-default transition-colors mb-6"
>
- Add a custom provider
+ {intl.formatMessage(i18n.addCustomProvider)}
{selectedProvider && (
@@ -181,7 +214,7 @@ export default function ProviderSelector({
- Add Custom Provider
+ {intl.formatMessage(i18n.addCustomProviderTitle)}
= ({
isExpanded = true,
onToggleExpanded,
}) => {
+ const intl = useIntl();
const { key, description, requirement } = parameter;
const defaultValue = parameter.default || '';
@@ -61,10 +143,10 @@ const ParameterInput: React.FC = ({
{isUnused && (
-
Unused
+
{intl.formatMessage(i18n.unused)}
)}
@@ -78,7 +160,7 @@ const ParameterInput: React.FC = ({
onDelete(key);
}}
className="p-1 text-red-500 hover:text-red-700 hover:bg-red-50 rounded transition-colors"
- title={`Delete parameter: ${key}`}
+ title={intl.formatMessage(i18n.deleteParameter, { key })}
>
@@ -91,17 +173,17 @@ const ParameterInput: React.FC = ({
@@ -109,7 +191,7 @@ const ParameterInput: React.FC
= ({
- Input Type
+ {intl.formatMessage(i18n.inputType)}
= ({
onChange(key, { input_type: e.target.value as Parameter['input_type'] })
}
>
- String
- Select
- Number
- Boolean
+ {intl.formatMessage(i18n.typeString)}
+ {intl.formatMessage(i18n.typeSelect)}
+ {intl.formatMessage(i18n.typeNumber)}
+ {intl.formatMessage(i18n.typeBoolean)}
- Requirement
+ {intl.formatMessage(i18n.requirement)}
= ({
onChange(key, { requirement: e.target.value as Parameter['requirement'] })
}
>
- Required
- Optional
+ {intl.formatMessage(i18n.required)}
+ {intl.formatMessage(i18n.optional)}
@@ -145,14 +227,14 @@ const ParameterInput: React.FC
= ({
{requirement === 'optional' && (
- Default Value
+ {intl.formatMessage(i18n.defaultValue)}
onChange(key, { default: e.target.value })}
className="w-full p-3 border rounded-lg bg-background-primary text-text-primary"
- placeholder="Enter default value"
+ placeholder={intl.formatMessage(i18n.defaultValuePlaceholder)}
/>
)}
@@ -162,7 +244,7 @@ const ParameterInput: React.FC = ({
{parameter.input_type === 'select' && (
- Options (one per line)
+ {intl.formatMessage(i18n.optionsLabel)}
)}
diff --git a/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx b/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx
index 11506699..7727098f 100644
--- a/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx
+++ b/ui/desktop/src/components/recipes/CreateEditRecipeModal.tsx
@@ -13,6 +13,98 @@ import { RecipeFormData } from './shared/recipeFormSchema';
import { toastSuccess, toastError } from '../../toasts';
import { saveRecipe } from '../../recipe/recipe_management';
import { errorMessage } from '../../utils/conversionUtils';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ createRecipeTitle: {
+ id: 'createEditRecipe.createRecipeTitle',
+ defaultMessage: 'Create Recipe',
+ },
+ viewEditRecipeTitle: {
+ id: 'createEditRecipe.viewEditRecipeTitle',
+ defaultMessage: 'View/edit recipe',
+ },
+ createSubtitle: {
+ id: 'createEditRecipe.createSubtitle',
+ defaultMessage: 'Create a new recipe to define agent behavior and capabilities for reusable chat sessions.',
+ },
+ editSubtitle: {
+ id: 'createEditRecipe.editSubtitle',
+ defaultMessage: "You can edit the recipe below to change the agent's behavior in a new session.",
+ },
+ learnMore: {
+ id: 'createEditRecipe.learnMore',
+ defaultMessage: 'Learn more',
+ },
+ copyLinkDescription: {
+ id: 'createEditRecipe.copyLinkDescription',
+ defaultMessage: 'Copy this link to share with friends or paste directly in Chrome to open',
+ },
+ copied: {
+ id: 'createEditRecipe.copied',
+ defaultMessage: 'Copied!',
+ },
+ copy: {
+ id: 'createEditRecipe.copy',
+ defaultMessage: 'Copy',
+ },
+ generatingDeeplink: {
+ id: 'createEditRecipe.generatingDeeplink',
+ defaultMessage: 'Generating deeplink...',
+ },
+ clickToGenerateDeeplink: {
+ id: 'createEditRecipe.clickToGenerateDeeplink',
+ defaultMessage: 'Click to generate deeplink',
+ },
+ close: {
+ id: 'createEditRecipe.close',
+ defaultMessage: 'Close',
+ },
+ saving: {
+ id: 'createEditRecipe.saving',
+ defaultMessage: 'Saving...',
+ },
+ saveRecipe: {
+ id: 'createEditRecipe.saveRecipe',
+ defaultMessage: 'Save Recipe',
+ },
+ saveAndRunRecipe: {
+ id: 'createEditRecipe.saveAndRunRecipe',
+ defaultMessage: 'Save & Run Recipe',
+ },
+ validationFailed: {
+ id: 'createEditRecipe.validationFailed',
+ defaultMessage: 'Validation Failed',
+ },
+ validationMsg: {
+ id: 'createEditRecipe.validationMsg',
+ defaultMessage: 'Please fill in all required fields and ensure JSON schema is valid.',
+ },
+ recipeSavedMsg: {
+ id: 'createEditRecipe.recipeSavedMsg',
+ defaultMessage: 'Recipe saved successfully',
+ },
+ saveFailed: {
+ id: 'createEditRecipe.saveFailed',
+ defaultMessage: 'Save Failed',
+ },
+ saveFailedMsg: {
+ id: 'createEditRecipe.saveFailedMsg',
+ defaultMessage: 'Failed to save recipe: {error}',
+ },
+ recipeSavedAndLaunchedMsg: {
+ id: 'createEditRecipe.recipeSavedAndLaunchedMsg',
+ defaultMessage: 'Recipe saved and launched successfully',
+ },
+ saveAndRunFailed: {
+ id: 'createEditRecipe.saveAndRunFailed',
+ defaultMessage: 'Save and Run Failed',
+ },
+ saveAndRunFailedMsg: {
+ id: 'createEditRecipe.saveAndRunFailedMsg',
+ defaultMessage: 'Failed to save and run recipe: {error}',
+ },
+});
interface CreateEditRecipeModalProps {
isOpen: boolean;
@@ -31,6 +123,7 @@ export default function CreateEditRecipeModal({
recipeId,
onRecipeSaved,
}: CreateEditRecipeModalProps) {
+ const intl = useIntl();
const getInitialValues = React.useCallback((): RecipeFormData => {
if (recipe) {
return {
@@ -313,8 +406,8 @@ export default function CreateEditRecipeModal({
const handleSaveRecipeClick = async () => {
if (!validateForm()) {
toastError({
- title: 'Validation Failed',
- msg: 'Please fill in all required fields and ensure JSON schema is valid.',
+ title: intl.formatMessage(i18n.validationFailed),
+ msg: intl.formatMessage(i18n.validationMsg),
});
return;
}
@@ -333,14 +426,14 @@ export default function CreateEditRecipeModal({
toastSuccess({
title: (recipe.title || '').trim(),
- msg: 'Recipe saved successfully',
+ msg: intl.formatMessage(i18n.recipeSavedMsg),
});
} catch (error) {
console.error('Failed to save recipe:', error);
toastError({
- title: 'Save Failed',
- msg: `Failed to save recipe: ${errorMessage(error, 'Unknown error')}`,
+ title: intl.formatMessage(i18n.saveFailed),
+ msg: intl.formatMessage(i18n.saveFailedMsg, { error: errorMessage(error, 'Unknown error') }),
traceback: errorMessage(error),
});
} finally {
@@ -351,8 +444,8 @@ export default function CreateEditRecipeModal({
const handleSaveAndRunRecipeClick = async () => {
if (!validateForm()) {
toastError({
- title: 'Validation Failed',
- msg: 'Please fill in all required fields and ensure JSON schema is valid.',
+ title: intl.formatMessage(i18n.validationFailed),
+ msg: intl.formatMessage(i18n.validationMsg),
});
return;
}
@@ -369,14 +462,14 @@ export default function CreateEditRecipeModal({
toastSuccess({
title: recipe.title,
- msg: 'Recipe saved and launched successfully',
+ msg: intl.formatMessage(i18n.recipeSavedAndLaunchedMsg),
});
} catch (error) {
console.error('Failed to save and run recipe:', error);
toastError({
- title: 'Save and Run Failed',
- msg: `Failed to save and run recipe: ${errorMessage(error, 'Unknown error')}`,
+ title: intl.formatMessage(i18n.saveAndRunFailed),
+ msg: intl.formatMessage(i18n.saveAndRunFailedMsg, { error: errorMessage(error, 'Unknown error') }),
traceback: errorMessage(error),
});
} finally {
@@ -397,19 +490,19 @@ export default function CreateEditRecipeModal({
- {isCreateMode ? 'Create Recipe' : 'View/edit recipe'}
+ {isCreateMode ? intl.formatMessage(i18n.createRecipeTitle) : intl.formatMessage(i18n.viewEditRecipeTitle)}
{isCreateMode
- ? 'Create a new recipe to define agent behavior and capabilities for reusable chat sessions.'
- : "You can edit the recipe below to change the agent's behavior in a new session."}{' '}
+ ? intl.formatMessage(i18n.createSubtitle)
+ : intl.formatMessage(i18n.editSubtitle)}{' '}
- Learn more
+ {intl.formatMessage(i18n.learnMore)}
@@ -434,7 +527,7 @@ export default function CreateEditRecipeModal({
- Copy this link to share with friends or paste directly in Chrome to open
+ {intl.formatMessage(i18n.copyLinkDescription)}
)}
- {copied ? 'Copied!' : 'Copy'}
+ {copied ? intl.formatMessage(i18n.copied) : intl.formatMessage(i18n.copy)}
@@ -460,8 +553,8 @@ export default function CreateEditRecipeModal({
className="text-sm truncate font-mono cursor-pointer text-text-primary"
>
{isGeneratingDeeplink
- ? 'Generating deeplink...'
- : deeplink || 'Click to generate deeplink'}
+ ? intl.formatMessage(i18n.generatingDeeplink)
+ : deeplink || intl.formatMessage(i18n.clickToGenerateDeeplink)}
)}
@@ -474,7 +567,7 @@ export default function CreateEditRecipeModal({
variant="ghost"
className="px-4 py-2 text-text-secondary rounded-lg hover:bg-background-secondary transition-colors"
>
- Close
+ {intl.formatMessage(i18n.close)}
@@ -486,7 +579,7 @@ export default function CreateEditRecipeModal({
className="inline-flex items-center justify-center gap-2 px-4 py-2"
>
- {isSaving ? 'Saving...' : 'Save Recipe'}
+ {isSaving ? intl.formatMessage(i18n.saving) : intl.formatMessage(i18n.saveRecipe)}
- {isSaving ? 'Saving...' : 'Save & Run Recipe'}
+ {isSaving ? intl.formatMessage(i18n.saving) : intl.formatMessage(i18n.saveAndRunRecipe)}
diff --git a/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx b/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx
index 0266c61a..9d1b4622 100644
--- a/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx
+++ b/ui/desktop/src/components/recipes/CreateRecipeFromSessionModal.tsx
@@ -11,6 +11,74 @@ import { RecipeParameter } from './shared/recipeFormSchema';
import { toastError } from '../../toasts';
import { saveRecipe } from '../../recipe/recipe_management';
import { errorMessage } from '../../utils/conversionUtils';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'createRecipeFromSession.title',
+ defaultMessage: 'Create Recipe from Session',
+ },
+ subtitle: {
+ id: 'createRecipeFromSession.subtitle',
+ defaultMessage: 'Create a reusable recipe based on your current conversation.',
+ },
+ analyzingTitle: {
+ id: 'createRecipeFromSession.analyzingTitle',
+ defaultMessage: 'Analyzing your conversation',
+ },
+ stageReading: {
+ id: 'createRecipeFromSession.stageReading',
+ defaultMessage: 'Reading your conversation...',
+ },
+ stageIdentifying: {
+ id: 'createRecipeFromSession.stageIdentifying',
+ defaultMessage: 'Identifying key patterns...',
+ },
+ stageExtracting: {
+ id: 'createRecipeFromSession.stageExtracting',
+ defaultMessage: 'Extracting main topics...',
+ },
+ stageGenerating: {
+ id: 'createRecipeFromSession.stageGenerating',
+ defaultMessage: 'Generating recipe structure...',
+ },
+ stageFinalizing: {
+ id: 'createRecipeFromSession.stageFinalizing',
+ defaultMessage: 'Finalizing details...',
+ },
+ stageComplete: {
+ id: 'createRecipeFromSession.stageComplete',
+ defaultMessage: 'Complete!',
+ },
+ extractingInsights: {
+ id: 'createRecipeFromSession.extractingInsights',
+ defaultMessage: 'Extracting insights from your chat',
+ },
+ cancel: {
+ id: 'createRecipeFromSession.cancel',
+ defaultMessage: 'Cancel',
+ },
+ creating: {
+ id: 'createRecipeFromSession.creating',
+ defaultMessage: 'Creating...',
+ },
+ createRecipe: {
+ id: 'createRecipeFromSession.createRecipe',
+ defaultMessage: 'Create Recipe',
+ },
+ createAndRunRecipe: {
+ id: 'createRecipeFromSession.createAndRunRecipe',
+ defaultMessage: 'Create & Run Recipe',
+ },
+ failedToCreateTitle: {
+ id: 'createRecipeFromSession.failedToCreateTitle',
+ defaultMessage: 'Failed to create recipe',
+ },
+ failedToCreateDefaultMsg: {
+ id: 'createRecipeFromSession.failedToCreateDefaultMsg',
+ defaultMessage: 'An unexpected error occurred while creating the recipe. Please try again.',
+ },
+});
interface CreateRecipeFromSessionModalProps {
isOpen: boolean;
@@ -25,6 +93,7 @@ export default function CreateRecipeFromSessionModal({
sessionId,
onRecipeCreated,
}: CreateRecipeFromSessionModalProps) {
+ const intl = useIntl();
const [isCreating, setIsCreating] = useState(false);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [analysisStage, setAnalysisStage] = useState('');
@@ -59,11 +128,11 @@ export default function CreateRecipeFromSessionModal({
// Create a sequence of analysis stages for better UX
const stages = [
- 'Reading your conversation...',
- 'Identifying key patterns...',
- 'Extracting main topics...',
- 'Generating recipe structure...',
- 'Finalizing details...',
+ intl.formatMessage(i18n.stageReading),
+ intl.formatMessage(i18n.stageIdentifying),
+ intl.formatMessage(i18n.stageExtracting),
+ intl.formatMessage(i18n.stageGenerating),
+ intl.formatMessage(i18n.stageFinalizing),
];
let currentStageIndex = 0;
@@ -82,7 +151,7 @@ export default function CreateRecipeFromSessionModal({
})
.then((response) => {
clearInterval(stageInterval);
- setAnalysisStage('Complete!');
+ setAnalysisStage(intl.formatMessage(i18n.stageComplete));
if (response.data?.recipe) {
const recipe = response.data.recipe;
@@ -118,7 +187,7 @@ export default function CreateRecipeFromSessionModal({
}, 500); // Brief delay to show completion
});
}
- }, [isOpen, sessionId, hasAnalyzed, form]);
+ }, [isOpen, sessionId, hasAnalyzed, form, intl]);
// Reset analysis state when modal closes
useEffect(() => {
@@ -209,10 +278,10 @@ export default function CreateRecipeFromSessionModal({
} catch (error) {
console.error('Failed to create recipe:', error);
toastError({
- title: 'Failed to create recipe',
+ title: intl.formatMessage(i18n.failedToCreateTitle),
msg: errorMessage(
error,
- 'An unexpected error occurred while creating the recipe. Please try again.'
+ intl.formatMessage(i18n.failedToCreateDefaultMsg)
),
});
} finally {
@@ -238,9 +307,9 @@ export default function CreateRecipeFromSessionModal({
-
Create Recipe from Session
+
{intl.formatMessage(i18n.title)}
- Create a reusable recipe based on your current conversation.
+ {intl.formatMessage(i18n.subtitle)}
@@ -271,7 +340,7 @@ export default function CreateRecipeFromSessionModal({
className="text-lg font-medium text-text-primary"
data-testid="analyzing-title"
>
- Analyzing your conversation
+ {intl.formatMessage(i18n.analyzingTitle)}
- Extracting insights from your chat
+ {intl.formatMessage(i18n.extractingInsights)}
) : (
@@ -303,7 +372,7 @@ export default function CreateRecipeFromSessionModal({
className="px-4 py-2 text-text-secondary rounded-lg hover:bg-background-secondary transition-colors"
data-testid="cancel-button"
>
- Cancel
+ {intl.formatMessage(i18n.cancel)}
@@ -319,7 +388,7 @@ export default function CreateRecipeFromSessionModal({
data-testid="create-recipe-button"
>
- {isCreating ? 'Creating...' : 'Create Recipe'}
+ {isCreating ? intl.formatMessage(i18n.creating) : intl.formatMessage(i18n.createRecipe)}
{
@@ -330,7 +399,7 @@ export default function CreateRecipeFromSessionModal({
data-testid="create-and-run-recipe-button"
>
- {isCreating ? 'Creating...' : 'Create & Run Recipe'}
+ {isCreating ? intl.formatMessage(i18n.creating) : intl.formatMessage(i18n.createAndRunRecipe)}
>
)}
diff --git a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
index adf698d8..174a0f0c 100644
--- a/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
+++ b/ui/desktop/src/components/recipes/ImportRecipeForm.tsx
@@ -10,6 +10,66 @@ import { useEscapeKey } from '../../hooks/useEscapeKey';
import { getRecipeJsonSchema } from '../../recipe/validation';
import { saveRecipe } from '../../recipe/recipe_management';
import { errorMessage } from '../../utils/conversionUtils';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ importRecipeTitle: {
+ id: 'importRecipeForm.importRecipeTitle',
+ defaultMessage: 'Import Recipe',
+ },
+ recipeDeeplinkLabel: {
+ id: 'importRecipeForm.recipeDeeplinkLabel',
+ defaultMessage: 'Recipe Deeplink',
+ },
+ deeplinkPlaceholder: {
+ id: 'importRecipeForm.deeplinkPlaceholder',
+ defaultMessage: 'Paste your goose://recipe?config=... deeplink here',
+ },
+ deeplinkHint: {
+ id: 'importRecipeForm.deeplinkHint',
+ defaultMessage: 'Paste a recipe deeplink starting with "goose://recipe?config="',
+ },
+ or: {
+ id: 'importRecipeForm.or',
+ defaultMessage: 'OR',
+ },
+ recipeFileLabel: {
+ id: 'importRecipeForm.recipeFileLabel',
+ defaultMessage: 'Recipe File',
+ },
+ recipeFileHint: {
+ id: 'importRecipeForm.recipeFileHint',
+ defaultMessage: 'Upload a YAML or JSON file containing the recipe structure',
+ },
+ example: {
+ id: 'importRecipeForm.example',
+ defaultMessage: 'example',
+ },
+ reviewWarning: {
+ id: 'importRecipeForm.reviewWarning',
+ defaultMessage: 'Ensure you review contents of recipe files before adding them to your goose interface.',
+ },
+ cancel: {
+ id: 'importRecipeForm.cancel',
+ defaultMessage: 'Cancel',
+ },
+ importing: {
+ id: 'importRecipeForm.importing',
+ defaultMessage: 'Importing...',
+ },
+ importRecipeButton: {
+ id: 'importRecipeForm.importRecipeButton',
+ defaultMessage: 'Import Recipe',
+ },
+ expectedRecipeStructure: {
+ id: 'importRecipeForm.expectedRecipeStructure',
+ defaultMessage: 'Expected Recipe Structure',
+ },
+ schemaDescription: {
+ id: 'importRecipeForm.schemaDescription',
+ defaultMessage: 'Your YAML or JSON file should follow this structure. Required fields are: title, description, and either instructions or prompt.',
+ },
+});
interface ImportRecipeFormProps {
isOpen: boolean;
@@ -40,6 +100,7 @@ const importRecipeSchema = z
});
export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportRecipeFormProps) {
+ const intl = useIntl();
const [importing, setImporting] = useState(false);
const [showSchemaModal, setShowSchemaModal] = useState(false);
@@ -147,7 +208,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
<>
-
Import Recipe
+
{intl.formatMessage(i18n.importRecipeTitle)}
- OR
+ {intl.formatMessage(i18n.or)}
@@ -227,7 +288,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
htmlFor="import-recipe-file"
className="block text-sm font-medium text-text-primary mb-3"
>
- Recipe File
+ {intl.formatMessage(i18n.recipeFileLabel)}
- Upload a YAML or JSON file containing the recipe structure
+ {intl.formatMessage(i18n.recipeFileHint)}
- example
+ {intl.formatMessage(i18n.example)}
{field.state.meta.errors.length > 0 && (
@@ -276,14 +337,13 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
- Ensure you review contents of recipe files before adding them to your goose
- interface.
+ {intl.formatMessage(i18n.reviewWarning)}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
[state.canSubmit, state.isSubmitting]}
@@ -294,7 +354,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
disabled={!canSubmit || importing || isSubmitting}
variant="default"
>
- {importing || isSubmitting ? 'Importing...' : 'Import Recipe'}
+ {importing || isSubmitting ? intl.formatMessage(i18n.importing) : intl.formatMessage(i18n.importRecipeButton)}
)}
@@ -308,7 +368,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
-
Expected Recipe Structure
+ {intl.formatMessage(i18n.expectedRecipeStructure)}
setShowSchemaModal(false)}
@@ -318,8 +378,7 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
- Your YAML or JSON file should follow this structure. Required fields are: title,
- description, and either instructions or prompt.
+ {intl.formatMessage(i18n.schemaDescription)}
@@ -334,10 +393,11 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
}
export function ImportRecipeButton({ onClick }: { onClick: () => void }) {
+ const intl = useIntl();
return (
- Import Recipe
+ {intl.formatMessage(i18n.importRecipeButton)}
);
}
diff --git a/ui/desktop/src/components/recipes/RecipeActivityEditor.tsx b/ui/desktop/src/components/recipes/RecipeActivityEditor.tsx
index 4cbc6523..d96bdb96 100644
--- a/ui/desktop/src/components/recipes/RecipeActivityEditor.tsx
+++ b/ui/desktop/src/components/recipes/RecipeActivityEditor.tsx
@@ -1,5 +1,45 @@
import { useState, useEffect } from 'react';
import { Button } from '../ui/button';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ activitiesLabel: {
+ id: 'recipeActivityEditor.activitiesLabel',
+ defaultMessage: 'Activities',
+ },
+ activitiesDescription: {
+ id: 'recipeActivityEditor.activitiesDescription',
+ defaultMessage: 'The top-line prompts and activity buttons that will display in the recipe chat window.',
+ },
+ messageLabel: {
+ id: 'recipeActivityEditor.messageLabel',
+ defaultMessage: 'Message',
+ },
+ messageDescription: {
+ id: 'recipeActivityEditor.messageDescription',
+ defaultMessage: 'A formatted message that will appear at the top of the recipe. Supports markdown formatting.',
+ },
+ messagePlaceholder: {
+ id: 'recipeActivityEditor.messagePlaceholder',
+ defaultMessage: 'Enter a user facing introduction message for your recipe (supports **bold**, *italic*, `code`, etc.)',
+ },
+ activityButtonsLabel: {
+ id: 'recipeActivityEditor.activityButtonsLabel',
+ defaultMessage: 'Activity Buttons',
+ },
+ activityButtonsDescription: {
+ id: 'recipeActivityEditor.activityButtonsDescription',
+ defaultMessage: 'Clickable buttons that will appear below the message to help users interact with your recipe.',
+ },
+ addNewActivityPlaceholder: {
+ id: 'recipeActivityEditor.addNewActivityPlaceholder',
+ defaultMessage: 'Add new activity...',
+ },
+ addActivity: {
+ id: 'recipeActivityEditor.addActivity',
+ defaultMessage: 'Add activity',
+ },
+});
export default function RecipeActivityEditor({
activities = [],
@@ -10,6 +50,7 @@ export default function RecipeActivityEditor({
setActivities: (prev: string[]) => void;
onBlur?: () => void;
}) {
+ const intl = useIntl();
const [newActivity, setNewActivity] = useState('');
const [messageContent, setMessageContent] = useState('');
@@ -61,20 +102,19 @@ export default function RecipeActivityEditor({
return (
- Activities
+ {intl.formatMessage(i18n.activitiesLabel)}
- The top-line prompts and activity buttons that will display in the recipe chat window.
+ {intl.formatMessage(i18n.activitiesDescription)}
{/* Message Field */}
- Message
+ {intl.formatMessage(i18n.messageLabel)}
- A formatted message that will appear at the top of the recipe. Supports markdown
- formatting.
+ {intl.formatMessage(i18n.messageDescription)}
handleMessageChange(e.target.value)}
onBlur={onBlur}
className="w-full px-4 py-3 border rounded-lg bg-background-primary text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-border-secondary resize-vertical"
- placeholder="Enter a user facing introduction message for your recipe (supports **bold**, *italic*, `code`, etc.)"
+ placeholder={intl.formatMessage(i18n.messagePlaceholder)}
rows={3}
autoCorrect="off"
autoCapitalize="off"
@@ -94,11 +134,10 @@ export default function RecipeActivityEditor({
- Activity Buttons
+ {intl.formatMessage(i18n.activityButtonsLabel)}
- Clickable buttons that will appear below the message to help users interact with your
- recipe.
+ {intl.formatMessage(i18n.activityButtonsDescription)}
@@ -132,7 +171,7 @@ export default function RecipeActivityEditor({
onKeyPress={(e) => e.key === 'Enter' && handleAddActivity()}
onBlur={onBlur}
className="flex-1 px-3 py-2 border border-border-primary rounded-lg bg-background-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
- placeholder="Add new activity..."
+ placeholder={intl.formatMessage(i18n.addNewActivityPlaceholder)}
/>
- Add activity
+ {intl.formatMessage(i18n.addActivity)}
diff --git a/ui/desktop/src/components/recipes/RecipesView.tsx b/ui/desktop/src/components/recipes/RecipesView.tsx
index a167a85d..e9e97782 100644
--- a/ui/desktop/src/components/recipes/RecipesView.tsx
+++ b/ui/desktop/src/components/recipes/RecipesView.tsx
@@ -59,8 +59,245 @@ import {
import { getSearchShortcutText } from '../../utils/keyboardShortcuts';
import { errorMessage } from '../../utils/conversionUtils';
import { AppEvents } from '../../constants/events';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ deleteRecipeTitle: {
+ id: 'recipesView.deleteRecipeTitle',
+ defaultMessage: 'Delete Recipe',
+ },
+ deleteRecipeConfirm: {
+ id: 'recipesView.deleteRecipeConfirm',
+ defaultMessage: 'Are you sure you want to delete "{title}"?',
+ },
+ deleteRecipeDetail: {
+ id: 'recipesView.deleteRecipeDetail',
+ defaultMessage: 'Recipe file will be deleted.',
+ },
+ recipeDeletedSuccess: {
+ id: 'recipesView.recipeDeletedSuccess',
+ defaultMessage: 'Recipe deleted successfully',
+ },
+ deeplinkCopiedTitle: {
+ id: 'recipesView.deeplinkCopiedTitle',
+ defaultMessage: 'Deeplink copied',
+ },
+ deeplinkCopiedMsg: {
+ id: 'recipesView.deeplinkCopiedMsg',
+ defaultMessage: 'Recipe deeplink has been copied to clipboard',
+ },
+ copyFailedTitle: {
+ id: 'recipesView.copyFailedTitle',
+ defaultMessage: 'Copy failed',
+ },
+ copyDeeplinkFailedMsg: {
+ id: 'recipesView.copyDeeplinkFailedMsg',
+ defaultMessage: 'Failed to copy deeplink to clipboard',
+ },
+ yamlCopiedTitle: {
+ id: 'recipesView.yamlCopiedTitle',
+ defaultMessage: 'YAML copied',
+ },
+ yamlCopiedMsg: {
+ id: 'recipesView.yamlCopiedMsg',
+ defaultMessage: 'Recipe YAML has been copied to clipboard',
+ },
+ copyYamlFailedMsg: {
+ id: 'recipesView.copyYamlFailedMsg',
+ defaultMessage: 'Failed to copy recipe YAML to clipboard',
+ },
+ exportRecipeDialogTitle: {
+ id: 'recipesView.exportRecipeDialogTitle',
+ defaultMessage: 'Export Recipe',
+ },
+ yamlFiles: {
+ id: 'recipesView.yamlFiles',
+ defaultMessage: 'YAML Files',
+ },
+ allFiles: {
+ id: 'recipesView.allFiles',
+ defaultMessage: 'All Files',
+ },
+ recipeExportedTitle: {
+ id: 'recipesView.recipeExportedTitle',
+ defaultMessage: 'Recipe exported',
+ },
+ recipeExportedMsg: {
+ id: 'recipesView.recipeExportedMsg',
+ defaultMessage: 'Recipe saved to {filePath}',
+ },
+ exportFailedTitle: {
+ id: 'recipesView.exportFailedTitle',
+ defaultMessage: 'Export failed',
+ },
+ exportFailedMsg: {
+ id: 'recipesView.exportFailedMsg',
+ defaultMessage: 'Failed to export recipe to file',
+ },
+ scheduleSavedTitle: {
+ id: 'recipesView.scheduleSavedTitle',
+ defaultMessage: 'Schedule saved',
+ },
+ scheduleSavedMsg: {
+ id: 'recipesView.scheduleSavedMsg',
+ defaultMessage: 'Recipe will run {schedule}',
+ },
+ scheduleRemovedTitle: {
+ id: 'recipesView.scheduleRemovedTitle',
+ defaultMessage: 'Schedule removed',
+ },
+ scheduleRemovedMsg: {
+ id: 'recipesView.scheduleRemovedMsg',
+ defaultMessage: 'Recipe will no longer run automatically',
+ },
+ slashCommandSavedTitle: {
+ id: 'recipesView.slashCommandSavedTitle',
+ defaultMessage: 'Slash command saved',
+ },
+ slashCommandSavedMsg: {
+ id: 'recipesView.slashCommandSavedMsg',
+ defaultMessage: 'Use /{command} to run this recipe',
+ },
+ slashCommandRemovedTitle: {
+ id: 'recipesView.slashCommandRemovedTitle',
+ defaultMessage: 'Slash command removed',
+ },
+ slashCommandRemovedMsg: {
+ id: 'recipesView.slashCommandRemovedMsg',
+ defaultMessage: 'Recipe slash command has been removed',
+ },
+ runs: {
+ id: 'recipesView.runs',
+ defaultMessage: 'Runs {schedule}',
+ },
+ editSlashCommand: {
+ id: 'recipesView.editSlashCommand',
+ defaultMessage: 'Edit slash command',
+ },
+ addSlashCommand: {
+ id: 'recipesView.addSlashCommand',
+ defaultMessage: 'Add slash command',
+ },
+ useRecipe: {
+ id: 'recipesView.useRecipe',
+ defaultMessage: 'Use recipe',
+ },
+ openInNewWindow: {
+ id: 'recipesView.openInNewWindow',
+ defaultMessage: 'Open in new window',
+ },
+ editRecipe: {
+ id: 'recipesView.editRecipe',
+ defaultMessage: 'Edit recipe',
+ },
+ shareRecipe: {
+ id: 'recipesView.shareRecipe',
+ defaultMessage: 'Share recipe',
+ },
+ copyDeeplink: {
+ id: 'recipesView.copyDeeplink',
+ defaultMessage: 'Copy Deeplink',
+ },
+ copyYaml: {
+ id: 'recipesView.copyYaml',
+ defaultMessage: 'Copy YAML',
+ },
+ exportToFile: {
+ id: 'recipesView.exportToFile',
+ defaultMessage: 'Export to File',
+ },
+ editSchedule: {
+ id: 'recipesView.editSchedule',
+ defaultMessage: 'Edit schedule',
+ },
+ addSchedule: {
+ id: 'recipesView.addSchedule',
+ defaultMessage: 'Add schedule',
+ },
+ deleteRecipe: {
+ id: 'recipesView.deleteRecipe',
+ defaultMessage: 'Delete recipe',
+ },
+ errorLoadingRecipes: {
+ id: 'recipesView.errorLoadingRecipes',
+ defaultMessage: 'Error Loading Recipes',
+ },
+ tryAgain: {
+ id: 'recipesView.tryAgain',
+ defaultMessage: 'Try Again',
+ },
+ noSavedRecipes: {
+ id: 'recipesView.noSavedRecipes',
+ defaultMessage: 'No saved recipes',
+ },
+ noSavedRecipesDescription: {
+ id: 'recipesView.noSavedRecipesDescription',
+ defaultMessage: 'Recipe saved from chats will show up here.',
+ },
+ noMatchingRecipes: {
+ id: 'recipesView.noMatchingRecipes',
+ defaultMessage: 'No matching recipes found',
+ },
+ adjustSearchTerms: {
+ id: 'recipesView.adjustSearchTerms',
+ defaultMessage: 'Try adjusting your search terms',
+ },
+ recipesTitle: {
+ id: 'recipesView.recipesTitle',
+ defaultMessage: 'Recipes',
+ },
+ createRecipe: {
+ id: 'recipesView.createRecipe',
+ defaultMessage: 'Create Recipe',
+ },
+ recipesDescription: {
+ id: 'recipesView.recipesDescription',
+ defaultMessage: 'View and manage your saved recipes to quickly start new sessions with predefined configurations. {shortcut} to search.',
+ },
+ searchRecipesPlaceholder: {
+ id: 'recipesView.searchRecipesPlaceholder',
+ defaultMessage: 'Search recipes...',
+ },
+ scheduleDialogTitle: {
+ id: 'recipesView.scheduleDialogTitle',
+ defaultMessage: '{action} Schedule',
+ },
+ removeSchedule: {
+ id: 'recipesView.removeSchedule',
+ defaultMessage: 'Remove Schedule',
+ },
+ cancel: {
+ id: 'recipesView.cancel',
+ defaultMessage: 'Cancel',
+ },
+ save: {
+ id: 'recipesView.save',
+ defaultMessage: 'Save',
+ },
+ slashCommandTitle: {
+ id: 'recipesView.slashCommandTitle',
+ defaultMessage: 'Slash Command',
+ },
+ slashCommandDescription: {
+ id: 'recipesView.slashCommandDescription',
+ defaultMessage: 'Set a slash command to quickly run this recipe from any chat',
+ },
+ slashCommandPlaceholder: {
+ id: 'recipesView.slashCommandPlaceholder',
+ defaultMessage: 'command-name',
+ },
+ slashCommandUsageHint: {
+ id: 'recipesView.slashCommandUsageHint',
+ defaultMessage: 'Use /{command} in any chat to run this recipe',
+ },
+ remove: {
+ id: 'recipesView.remove',
+ defaultMessage: 'Remove',
+ },
+});
export default function RecipesView() {
+ const intl = useIntl();
const setView = useNavigation();
const [savedRecipes, setSavedRecipes] = useState
([]);
const [loading, setLoading] = useState(true);
@@ -185,11 +422,11 @@ export default function RecipesView() {
const handleDeleteRecipe = async (recipeManifest: RecipeManifest) => {
const result = await window.electron.showMessageBox({
type: 'warning',
- buttons: ['Cancel', 'Delete'],
+ buttons: [intl.formatMessage(i18n.cancel), 'Delete'],
defaultId: 0,
- title: 'Delete Recipe',
- message: `Are you sure you want to delete "${recipeManifest.recipe.title}"?`,
- detail: 'Recipe file will be deleted.',
+ title: intl.formatMessage(i18n.deleteRecipeTitle),
+ message: intl.formatMessage(i18n.deleteRecipeConfirm, { title: recipeManifest.recipe.title }),
+ detail: intl.formatMessage(i18n.deleteRecipeDetail),
});
if (result.response !== 1) {
@@ -202,7 +439,7 @@ export default function RecipesView() {
await loadSavedRecipes();
toastSuccess({
title: recipeManifest.recipe.title,
- msg: 'Recipe deleted successfully',
+ msg: intl.formatMessage(i18n.recipeDeletedSuccess),
});
} catch (err) {
console.error('Failed to delete recipe:', err);
@@ -231,15 +468,15 @@ export default function RecipesView() {
await navigator.clipboard.writeText(deeplink);
trackRecipeDeeplinkCopied(true);
toastSuccess({
- title: 'Deeplink copied',
- msg: 'Recipe deeplink has been copied to clipboard',
+ title: intl.formatMessage(i18n.deeplinkCopiedTitle),
+ msg: intl.formatMessage(i18n.deeplinkCopiedMsg),
});
} catch (error) {
console.error('Failed to copy deeplink:', error);
trackRecipeDeeplinkCopied(false, getErrorType(error));
toastError({
- title: 'Copy failed',
- msg: 'Failed to copy deeplink to clipboard',
+ title: intl.formatMessage(i18n.copyFailedTitle),
+ msg: intl.formatMessage(i18n.copyDeeplinkFailedMsg),
});
}
};
@@ -258,15 +495,15 @@ export default function RecipesView() {
await navigator.clipboard.writeText(response.data.yaml);
trackRecipeYamlCopied(true);
toastSuccess({
- title: 'YAML copied',
- msg: 'Recipe YAML has been copied to clipboard',
+ title: intl.formatMessage(i18n.yamlCopiedTitle),
+ msg: intl.formatMessage(i18n.yamlCopiedMsg),
});
} catch (error) {
console.error('Failed to copy YAML:', error);
trackRecipeYamlCopied(false, getErrorType(error));
toastError({
- title: 'Copy failed',
- msg: 'Failed to copy recipe YAML to clipboard',
+ title: intl.formatMessage(i18n.copyFailedTitle),
+ msg: intl.formatMessage(i18n.copyYamlFailedMsg),
});
}
};
@@ -290,11 +527,11 @@ export default function RecipesView() {
const filename = `${sanitizedTitle}.yaml`;
const result = await window.electron.showSaveDialog({
- title: 'Export Recipe',
+ title: intl.formatMessage(i18n.exportRecipeDialogTitle),
defaultPath: filename,
filters: [
- { name: 'YAML Files', extensions: ['yaml', 'yml'] },
- { name: 'All Files', extensions: ['*'] },
+ { name: intl.formatMessage(i18n.yamlFiles), extensions: ['yaml', 'yml'] },
+ { name: intl.formatMessage(i18n.allFiles), extensions: ['*'] },
],
});
@@ -302,16 +539,16 @@ export default function RecipesView() {
await window.electron.writeFile(result.filePath, response.data.yaml);
trackRecipeExportedToFile(true);
toastSuccess({
- title: 'Recipe exported',
- msg: `Recipe saved to ${result.filePath}`,
+ title: intl.formatMessage(i18n.recipeExportedTitle),
+ msg: intl.formatMessage(i18n.recipeExportedMsg, { filePath: result.filePath }),
});
}
} catch (error) {
console.error('Failed to export recipe:', error);
trackRecipeExportedToFile(false, getErrorType(error));
toastError({
- title: 'Export failed',
- msg: 'Failed to export recipe to file',
+ title: intl.formatMessage(i18n.exportFailedTitle),
+ msg: intl.formatMessage(i18n.exportFailedMsg),
});
}
};
@@ -337,8 +574,8 @@ export default function RecipesView() {
trackRecipeScheduled(true, action);
toastSuccess({
- title: 'Schedule saved',
- msg: `Recipe will run ${getReadableCron(scheduleCron)}`,
+ title: intl.formatMessage(i18n.scheduleSavedTitle),
+ msg: intl.formatMessage(i18n.scheduleSavedMsg, { schedule: getReadableCron(scheduleCron) }),
});
setShowScheduleDialog(false);
@@ -365,8 +602,8 @@ export default function RecipesView() {
trackRecipeScheduled(true, 'remove');
toastSuccess({
- title: 'Schedule removed',
- msg: 'Recipe will no longer run automatically',
+ title: intl.formatMessage(i18n.scheduleRemovedTitle),
+ msg: intl.formatMessage(i18n.scheduleRemovedMsg),
});
setShowScheduleDialog(false);
@@ -405,8 +642,8 @@ export default function RecipesView() {
trackRecipeSlashCommandSet(true, action);
toastSuccess({
- title: 'Slash command saved',
- msg: slashCommand ? `Use /${slashCommand} to run this recipe` : 'Slash command removed',
+ title: intl.formatMessage(i18n.slashCommandSavedTitle),
+ msg: slashCommand ? intl.formatMessage(i18n.slashCommandSavedMsg, { command: slashCommand }) : intl.formatMessage(i18n.slashCommandRemovedMsg),
});
setShowSlashCommandDialog(false);
@@ -433,8 +670,8 @@ export default function RecipesView() {
trackRecipeSlashCommandSet(true, 'remove');
toastSuccess({
- title: 'Slash command removed',
- msg: 'Recipe slash command has been removed',
+ title: intl.formatMessage(i18n.slashCommandRemovedTitle),
+ msg: intl.formatMessage(i18n.slashCommandRemovedMsg),
});
setShowSlashCommandDialog(false);
@@ -480,7 +717,7 @@ export default function RecipesView() {
{schedule_cron && (
- Runs {getReadableCron(schedule_cron)}
+ {intl.formatMessage(i18n.runs, { schedule: getReadableCron(schedule_cron) })}
)}
{slash_command && (
@@ -501,7 +738,7 @@ export default function RecipesView() {
variant={slash_command ? 'default' : 'outline'}
size="sm"
className="h-8 w-8 p-0"
- title={slash_command ? 'Edit slash command' : 'Add slash command'}
+ title={slash_command ? intl.formatMessage(i18n.editSlashCommand) : intl.formatMessage(i18n.addSlashCommand)}
>
@@ -514,7 +751,7 @@ export default function RecipesView() {
}}
size="sm"
className="h-8 w-8 p-0"
- title="Use recipe"
+ title={intl.formatMessage(i18n.useRecipe)}
>
@@ -526,7 +763,7 @@ export default function RecipesView() {
variant="outline"
size="sm"
className="h-8 w-8 p-0"
- title="Open in new window"
+ title={intl.formatMessage(i18n.openInNewWindow)}
>
@@ -538,7 +775,7 @@ export default function RecipesView() {
variant="outline"
size="sm"
className="h-8 w-8 p-0"
- title="Edit recipe"
+ title={intl.formatMessage(i18n.editRecipe)}
>
@@ -549,7 +786,7 @@ export default function RecipesView() {
variant="outline"
size="sm"
className="h-8 w-8 p-0"
- title="Share recipe"
+ title={intl.formatMessage(i18n.shareRecipe)}
>
@@ -557,16 +794,16 @@ export default function RecipesView() {
e.stopPropagation()}>
handleCopyDeeplink(recipeManifestResponse)}>
- Copy Deeplink
+ {intl.formatMessage(i18n.copyDeeplink)}
handleCopyYaml(recipeManifestResponse)}>
- Copy YAML
+ {intl.formatMessage(i18n.copyYaml)}
handleExportFile(recipeManifestResponse)}>
- Export to File
+ {intl.formatMessage(i18n.exportToFile)}
@@ -578,7 +815,7 @@ export default function RecipesView() {
variant={schedule_cron ? 'default' : 'outline'}
size="sm"
className="h-8 w-8 p-0"
- title={schedule_cron ? 'Edit schedule' : 'Add schedule'}
+ title={schedule_cron ? intl.formatMessage(i18n.editSchedule) : intl.formatMessage(i18n.addSchedule)}
>
@@ -590,7 +827,7 @@ export default function RecipesView() {
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
- title="Delete recipe"
+ title={intl.formatMessage(i18n.deleteRecipe)}
>
@@ -638,10 +875,10 @@ export default function RecipesView() {
return (
-
Error Loading Recipes
+
{intl.formatMessage(i18n.errorLoadingRecipes)}
{error}
- Try Again
+ {intl.formatMessage(i18n.tryAgain)}
);
@@ -650,8 +887,8 @@ export default function RecipesView() {
if (savedRecipes.length === 0) {
return (
-
No saved recipes
-
Recipe saved from chats will show up here.
+
{intl.formatMessage(i18n.noSavedRecipes)}
+
{intl.formatMessage(i18n.noSavedRecipesDescription)}
);
}
@@ -660,8 +897,8 @@ export default function RecipesView() {
return (
-
No matching recipes found
-
Try adjusting your search terms
+
{intl.formatMessage(i18n.noMatchingRecipes)}
+
{intl.formatMessage(i18n.adjustSearchTerms)}
);
}
@@ -685,7 +922,7 @@ export default function RecipesView() {
-
Recipes
+
{intl.formatMessage(i18n.recipesTitle)}
setShowCreateDialog(true)}
@@ -694,21 +931,20 @@ export default function RecipesView() {
className="flex items-center gap-2"
>
- Create Recipe
+ {intl.formatMessage(i18n.createRecipe)}
setShowImportDialog(true)} />
- View and manage your saved recipes to quickly start new sessions with predefined
- configurations. {getSearchShortcutText()} to search.
+ {intl.formatMessage(i18n.recipesDescription, { shortcut: getSearchShortcutText() })}
- setSearchTerm(term)} placeholder="Search recipes...">
+ setSearchTerm(term)} placeholder={intl.formatMessage(i18n.searchRecipesPlaceholder)}>
- {scheduleRecipeManifest.schedule_cron ? 'Edit' : 'Add'} Schedule
+ {intl.formatMessage(i18n.scheduleDialogTitle, { action: scheduleRecipeManifest.schedule_cron ? 'Edit' : 'Add' })}
@@ -776,14 +1012,14 @@ export default function RecipesView() {
{scheduleRecipeManifest.schedule_cron && (
- Remove Schedule
+ {intl.formatMessage(i18n.removeSchedule)}
)}
setShowScheduleDialog(false)}>
- Cancel
+ {intl.formatMessage(i18n.cancel)}
- Save
+ {intl.formatMessage(i18n.save)}
@@ -795,12 +1031,12 @@ export default function RecipesView() {
- Slash Command
+ {intl.formatMessage(i18n.slashCommandTitle)}
- Set a slash command to quickly run this recipe from any chat
+ {intl.formatMessage(i18n.slashCommandDescription)}
/
@@ -808,13 +1044,13 @@ export default function RecipesView() {
type="text"
value={slashCommand}
onChange={(e) => setSlashCommand(e.target.value)}
- placeholder="command-name"
+ placeholder={intl.formatMessage(i18n.slashCommandPlaceholder)}
className="flex-1 px-3 py-2 border rounded text-sm"
/>
{slashCommand && (
- Use /{slashCommand} in any chat to run this recipe
+ {intl.formatMessage(i18n.slashCommandUsageHint, { command: slashCommand })}
)}
@@ -822,13 +1058,13 @@ export default function RecipesView() {
{slashCommandRecipeManifest.slash_command && (
- Remove
+ {intl.formatMessage(i18n.remove)}
)}
setShowSlashCommandDialog(false)}>
- Cancel
+ {intl.formatMessage(i18n.cancel)}
- Save
+ {intl.formatMessage(i18n.save)}
diff --git a/ui/desktop/src/components/recipes/__tests__/CreateRecipeFromSessionModal.test.tsx b/ui/desktop/src/components/recipes/__tests__/CreateRecipeFromSessionModal.test.tsx
index 0b2ec726..00e998f6 100644
--- a/ui/desktop/src/components/recipes/__tests__/CreateRecipeFromSessionModal.test.tsx
+++ b/ui/desktop/src/components/recipes/__tests__/CreateRecipeFromSessionModal.test.tsx
@@ -1,9 +1,13 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, screen, waitFor } from '@testing-library/react';
+import { render, type RenderOptions, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CreateRecipeFromSessionModal from '../CreateRecipeFromSessionModal';
import { createRecipe } from '../../../api/sdk.gen';
import type { CreateRecipeResponse } from '../../../api/types.gen';
+import { IntlTestWrapper } from '../../../i18n/test-utils';
+
+const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) =>
+ render(ui, { wrapper: IntlTestWrapper, ...options });
vi.mock('../../../api/sdk.gen', () => ({
createRecipe: vi.fn(),
@@ -69,19 +73,19 @@ describe('CreateRecipeFromSessionModal', () => {
describe('Modal Rendering', () => {
it('renders modal when open', () => {
- render( );
+ renderWithIntl( );
expect(screen.getByTestId('create-recipe-modal')).toBeInTheDocument();
});
it('does not render when closed', () => {
- render( );
+ renderWithIntl( );
expect(screen.queryByTestId('create-recipe-modal')).not.toBeInTheDocument();
});
it('renders modal header with close button', () => {
- render( );
+ renderWithIntl( );
expect(screen.getByTestId('modal-header')).toBeInTheDocument();
expect(screen.getByTestId('close-button')).toBeInTheDocument();
@@ -89,7 +93,7 @@ describe('CreateRecipeFromSessionModal', () => {
it('calls onClose when close button is clicked', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await user.click(screen.getByTestId('close-button'));
expect(defaultProps.onClose).toHaveBeenCalled();
@@ -98,14 +102,14 @@ describe('CreateRecipeFromSessionModal', () => {
describe('Analysis Workflow', () => {
it('shows analyzing state initially', () => {
- render( );
+ renderWithIntl( );
expect(screen.getByTestId('analyzing-state')).toBeInTheDocument();
expect(screen.getByTestId('analyzing-title')).toBeInTheDocument();
});
it('displays analysis progress indicator', async () => {
- render( );
+ renderWithIntl( );
expect(screen.getByTestId('analysis-stage')).toBeInTheDocument();
@@ -119,13 +123,13 @@ describe('CreateRecipeFromSessionModal', () => {
});
it('shows loading indicator during analysis', () => {
- render( );
+ renderWithIntl( );
expect(screen.getByTestId('analysis-spinner')).toBeInTheDocument();
});
it('transitions to form state after analysis completes', async () => {
- render( );
+ renderWithIntl( );
await waitFor(
() => {
@@ -140,7 +144,7 @@ describe('CreateRecipeFromSessionModal', () => {
describe('Form Pre-filling', () => {
it('pre-fills form with analyzed data', async () => {
- render( );
+ renderWithIntl( );
// Wait for analysis to complete and form to be pre-filled
await waitFor(
@@ -157,7 +161,7 @@ describe('CreateRecipeFromSessionModal', () => {
});
it('shows recipe form fields after analysis', async () => {
- render( );
+ renderWithIntl( );
await waitFor(
() => {
@@ -176,7 +180,7 @@ describe('CreateRecipeFromSessionModal', () => {
describe('Form Interactions', () => {
it('allows editing form fields', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await waitFor(
() => {
@@ -194,7 +198,7 @@ describe('CreateRecipeFromSessionModal', () => {
it('validates required fields', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await waitFor(
() => {
@@ -213,7 +217,7 @@ describe('CreateRecipeFromSessionModal', () => {
describe('Recipe Creation', () => {
it('enables create button when form is valid', async () => {
- render( );
+ renderWithIntl( );
await waitFor(
() => {
@@ -226,7 +230,7 @@ describe('CreateRecipeFromSessionModal', () => {
it('creates recipe and closes modal when form is submitted', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await waitFor(
() => {
@@ -246,7 +250,7 @@ describe('CreateRecipeFromSessionModal', () => {
describe('Modal Footer', () => {
it('shows cancel button in all states', async () => {
- render( );
+ renderWithIntl( );
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
@@ -262,14 +266,14 @@ describe('CreateRecipeFromSessionModal', () => {
it('calls onClose when cancel button is clicked', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await user.click(screen.getByTestId('cancel-button'));
expect(defaultProps.onClose).toHaveBeenCalled();
});
it('shows different button states based on workflow stage', async () => {
- render( );
+ renderWithIntl( );
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
expect(screen.queryByTestId('create-recipe-button')).not.toBeInTheDocument();
@@ -287,14 +291,14 @@ describe('CreateRecipeFromSessionModal', () => {
describe('Error Handling', () => {
it('handles analysis errors gracefully', async () => {
- render( );
+ renderWithIntl( );
expect(screen.getByTestId('create-recipe-modal')).toBeInTheDocument();
});
it('handles form validation errors', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await waitFor(
() => {
diff --git a/ui/desktop/src/components/recipes/shared/CreateSubRecipeInline.tsx b/ui/desktop/src/components/recipes/shared/CreateSubRecipeInline.tsx
index 8be60b87..e02b70ab 100644
--- a/ui/desktop/src/components/recipes/shared/CreateSubRecipeInline.tsx
+++ b/ui/desktop/src/components/recipes/shared/CreateSubRecipeInline.tsx
@@ -8,6 +8,122 @@ import { Recipe } from '../../../recipe';
import { SubRecipeFormData } from './recipeFormSchema';
import { useEscapeKey } from '../../../hooks/useEscapeKey';
import KeyValueEditor from './KeyValueEditor';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'createSubRecipeInline.title',
+ defaultMessage: 'Create New Subrecipe',
+ },
+ subtitle: {
+ id: 'createSubRecipeInline.subtitle',
+ defaultMessage: 'Create a simple recipe to use as a callable tool in your main recipe',
+ },
+ closeModal: {
+ id: 'createSubRecipeInline.closeModal',
+ defaultMessage: 'Close create subrecipe modal',
+ },
+ nameLabel: {
+ id: 'createSubRecipeInline.nameLabel',
+ defaultMessage: 'Name',
+ },
+ namePlaceholder: {
+ id: 'createSubRecipeInline.namePlaceholder',
+ defaultMessage: 'e.g., security_scan',
+ },
+ nameHint: {
+ id: 'createSubRecipeInline.nameHint',
+ defaultMessage: 'Unique identifier used to generate the tool name',
+ },
+ recipeTitleLabel: {
+ id: 'createSubRecipeInline.recipeTitleLabel',
+ defaultMessage: 'Recipe Title',
+ },
+ recipeTitlePlaceholder: {
+ id: 'createSubRecipeInline.recipeTitlePlaceholder',
+ defaultMessage: 'e.g., Security Analysis Tool',
+ },
+ recipeDescriptionLabel: {
+ id: 'createSubRecipeInline.recipeDescriptionLabel',
+ defaultMessage: 'Recipe Description',
+ },
+ recipeDescriptionPlaceholder: {
+ id: 'createSubRecipeInline.recipeDescriptionPlaceholder',
+ defaultMessage: 'What this recipe does when executed',
+ },
+ instructionsLabel: {
+ id: 'createSubRecipeInline.instructionsLabel',
+ defaultMessage: 'Instructions',
+ },
+ instructionsPlaceholder: {
+ id: 'createSubRecipeInline.instructionsPlaceholder',
+ defaultMessage: 'Instructions for the AI when this subrecipe is called...',
+ },
+ toolDescriptionLabel: {
+ id: 'createSubRecipeInline.toolDescriptionLabel',
+ defaultMessage: 'Tool Description',
+ },
+ toolDescriptionPlaceholder: {
+ id: 'createSubRecipeInline.toolDescriptionPlaceholder',
+ defaultMessage: 'Optional description shown when this is called as a tool',
+ },
+ sequentialLabel: {
+ id: 'createSubRecipeInline.sequentialLabel',
+ defaultMessage: 'Sequential when repeated',
+ },
+ sequentialHint: {
+ id: 'createSubRecipeInline.sequentialHint',
+ defaultMessage: '(Forces sequential execution of multiple instances)',
+ },
+ preconfiguredValues: {
+ id: 'createSubRecipeInline.preconfiguredValues',
+ defaultMessage: 'Pre-configured Values',
+ },
+ preconfiguredValuesHint: {
+ id: 'createSubRecipeInline.preconfiguredValuesHint',
+ defaultMessage: 'Optional parameter values that are always passed to the subrecipe',
+ },
+ cancel: {
+ id: 'createSubRecipeInline.cancel',
+ defaultMessage: 'Cancel',
+ },
+ creating: {
+ id: 'createSubRecipeInline.creating',
+ defaultMessage: 'Creating...',
+ },
+ createAndAdd: {
+ id: 'createSubRecipeInline.createAndAdd',
+ defaultMessage: 'Create & Add Subrecipe',
+ },
+ validationFailed: {
+ id: 'createSubRecipeInline.validationFailed',
+ defaultMessage: 'Validation Failed',
+ },
+ validationMsg: {
+ id: 'createSubRecipeInline.validationMsg',
+ defaultMessage: 'Name, title, recipe description, and instructions are required.',
+ },
+ duplicateName: {
+ id: 'createSubRecipeInline.duplicateName',
+ defaultMessage: 'Duplicate Name',
+ },
+ duplicateNameMsg: {
+ id: 'createSubRecipeInline.duplicateNameMsg',
+ defaultMessage: 'A subrecipe named "{name}" already exists. Please use a unique name.',
+ },
+ createdSuccess: {
+ id: 'createSubRecipeInline.createdSuccess',
+ defaultMessage: 'Subrecipe created successfully',
+ },
+ saveFailed: {
+ id: 'createSubRecipeInline.saveFailed',
+ defaultMessage: 'Save Failed',
+ },
+ saveFailedMsg: {
+ id: 'createSubRecipeInline.saveFailedMsg',
+ defaultMessage: 'Failed to save subrecipe: {error}',
+ },
+});
interface CreateSubRecipeInlineProps {
isOpen: boolean;
@@ -22,6 +138,7 @@ export default function CreateSubRecipeInline({
onSubRecipeSaved,
existingSubRecipes = [],
}: CreateSubRecipeInlineProps) {
+ const intl = useIntl();
useEscapeKey(isOpen, onClose);
const form = useForm({
@@ -53,8 +170,8 @@ export default function CreateSubRecipeInline({
!formValues.instructions.trim()
) {
toastError({
- title: 'Validation Failed',
- msg: 'Name, title, recipe description, and instructions are required.',
+ title: intl.formatMessage(i18n.validationFailed),
+ msg: intl.formatMessage(i18n.validationMsg),
});
return;
}
@@ -62,8 +179,8 @@ export default function CreateSubRecipeInline({
const trimmedName = name.trim();
if (existingSubRecipes.some((sr) => sr.name === trimmedName)) {
toastError({
- title: 'Duplicate Name',
- msg: `A subrecipe named "${trimmedName}" already exists. Please use a unique name.`,
+ title: intl.formatMessage(i18n.duplicateName),
+ msg: intl.formatMessage(i18n.duplicateNameMsg, { name: trimmedName }),
});
return;
}
@@ -89,7 +206,7 @@ export default function CreateSubRecipeInline({
toastSuccess({
title: formValues.title.trim(),
- msg: 'Subrecipe created successfully',
+ msg: intl.formatMessage(i18n.createdSuccess),
});
onSubRecipeSaved(subRecipe);
@@ -103,13 +220,13 @@ export default function CreateSubRecipeInline({
console.error('Failed to save subrecipe:', error);
toastError({
- title: 'Save Failed',
- msg: `Failed to save subrecipe: ${error instanceof Error ? error.message : 'Unknown error'}`,
+ title: intl.formatMessage(i18n.saveFailed),
+ msg: intl.formatMessage(i18n.saveFailedMsg, { error: error instanceof Error ? error.message : 'Unknown error' }),
});
} finally {
setIsSaving(false);
}
- }, [form, name, toolDescription, sequentialWhenRepeated, values, existingSubRecipes, onSubRecipeSaved, onClose]);
+ }, [form, name, toolDescription, sequentialWhenRepeated, values, existingSubRecipes, onSubRecipeSaved, onClose, intl]);
if (!isOpen) return null;
@@ -119,9 +236,9 @@ export default function CreateSubRecipeInline({
{/* Header */}
-
Create New Subrecipe
+
{intl.formatMessage(i18n.title)}
- Create a simple recipe to use as a callable tool in your main recipe
+ {intl.formatMessage(i18n.subtitle)}
@@ -143,7 +260,7 @@ export default function CreateSubRecipeInline({
htmlFor="subrecipe-name"
className="block text-sm font-medium text-text-standard mb-2"
>
- Name
*
+ {intl.formatMessage(i18n.nameLabel)}
*
setName(e.target.value)}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring"
- placeholder="e.g., security_scan"
+ placeholder={intl.formatMessage(i18n.namePlaceholder)}
/>
- Unique identifier used to generate the tool name
+ {intl.formatMessage(i18n.nameHint)}
@@ -166,7 +283,7 @@ export default function CreateSubRecipeInline({
htmlFor="subrecipe-title"
className="block text-sm font-medium text-text-standard mb-2"
>
- Recipe Title *
+ {intl.formatMessage(i18n.recipeTitleLabel)} *
field.handleChange(e.target.value)}
onBlur={field.handleBlur}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring"
- placeholder="e.g., Security Analysis Tool"
+ placeholder={intl.formatMessage(i18n.recipeTitlePlaceholder)}
/>
)}
@@ -189,7 +306,7 @@ export default function CreateSubRecipeInline({
htmlFor="recipe-description"
className="block text-sm font-medium text-text-standard mb-2"
>
- Recipe Description *
+ {intl.formatMessage(i18n.recipeDescriptionLabel)} *
field.handleChange(e.target.value)}
onBlur={field.handleBlur}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring"
- placeholder="What this recipe does when executed"
+ placeholder={intl.formatMessage(i18n.recipeDescriptionPlaceholder)}
/>
)}
@@ -212,7 +329,7 @@ export default function CreateSubRecipeInline({
htmlFor="subrecipe-instructions"
className="block text-sm font-medium text-text-standard mb-2"
>
- Instructions *
+ {intl.formatMessage(i18n.instructionsLabel)} *
field.handleChange(e.target.value)}
onBlur={field.handleBlur}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring resize-none font-mono text-sm"
- placeholder="Instructions for the AI when this subrecipe is called..."
+ placeholder={intl.formatMessage(i18n.instructionsPlaceholder)}
rows={8}
/>
@@ -233,14 +350,14 @@ export default function CreateSubRecipeInline({
htmlFor="tool-description"
className="block text-sm font-medium text-text-standard mb-2"
>
- Tool Description
+ {intl.formatMessage(i18n.toolDescriptionLabel)}
setToolDescription(e.target.value)}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring resize-none"
- placeholder="Optional description shown when this is called as a tool"
+ placeholder={intl.formatMessage(i18n.toolDescriptionPlaceholder)}
rows={2}
/>
@@ -255,20 +372,20 @@ export default function CreateSubRecipeInline({
className="w-4 h-4 border-border-subtle rounded focus:ring-2 focus:ring-ring"
/>
- Sequential when repeated
+ {intl.formatMessage(i18n.sequentialLabel)}
- (Forces sequential execution of multiple instances)
+ {intl.formatMessage(i18n.sequentialHint)}
{/* Pre-configured Values */}
- Pre-configured Values
+ {intl.formatMessage(i18n.preconfiguredValues)}
- Optional parameter values that are always passed to the subrecipe
+ {intl.formatMessage(i18n.preconfiguredValuesHint)}
@@ -277,7 +394,7 @@ export default function CreateSubRecipeInline({
{/* Footer */}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
- Creating...
+ {intl.formatMessage(i18n.creating)}
>
) : (
<>
- Create & Add Subrecipe
+ {intl.formatMessage(i18n.createAndAdd)}
>
)}
diff --git a/ui/desktop/src/components/recipes/shared/InstructionsEditor.tsx b/ui/desktop/src/components/recipes/shared/InstructionsEditor.tsx
index 96fc1f29..e511e984 100644
--- a/ui/desktop/src/components/recipes/shared/InstructionsEditor.tsx
+++ b/ui/desktop/src/components/recipes/shared/InstructionsEditor.tsx
@@ -1,6 +1,38 @@
import React, { useState } from 'react';
import { Button } from '../../ui/button';
import { useEscapeKey } from '../../../hooks/useEscapeKey';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'instructionsEditor.title',
+ defaultMessage: 'Instructions Editor',
+ },
+ label: {
+ id: 'instructionsEditor.label',
+ defaultMessage: 'Instructions',
+ },
+ insertExample: {
+ id: 'instructionsEditor.insertExample',
+ defaultMessage: 'Insert Example',
+ },
+ syntaxHelp: {
+ id: 'instructionsEditor.syntaxHelp',
+ defaultMessage: 'Use {code} syntax to define parameters that users can fill in',
+ },
+ placeholder: {
+ id: 'instructionsEditor.placeholder',
+ defaultMessage: 'Detailed instructions for the AI, hidden from the user',
+ },
+ cancel: {
+ id: 'instructionsEditor.cancel',
+ defaultMessage: 'Cancel',
+ },
+ save: {
+ id: 'instructionsEditor.save',
+ defaultMessage: 'Save Instructions',
+ },
+});
interface InstructionsEditorProps {
isOpen: boolean;
@@ -17,6 +49,7 @@ export default function InstructionsEditor({
onChange,
error,
}: InstructionsEditorProps) {
+ const intl = useIntl();
const [localValue, setLocalValue] = useState(value);
useEscapeKey(isOpen, onClose);
@@ -74,7 +107,7 @@ Use {{parameter_name}} syntax for any user-provided values.`;
>
-
Instructions Editor
+
{intl.formatMessage(i18n.title)}
- Instructions
+ {intl.formatMessage(i18n.label)}
- Insert Example
+ {intl.formatMessage(i18n.insertExample)}
- Use{' '}
- {`{{parameter_name}}`}{' '}
- syntax to define parameters that users can fill in
+ {intl.formatMessage(i18n.syntaxHelp, { code: '{{parameter_name}}' })}
@@ -112,7 +143,7 @@ Use {{parameter_name}} syntax for any user-provided values.`;
className={`w-full h-full min-h-[500px] p-3 border rounded-lg bg-background-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none font-mono text-sm ${
error ? 'border-red-500' : 'border-border-primary'
}`}
- placeholder="Detailed instructions for the AI, hidden from the user"
+ placeholder={intl.formatMessage(i18n.placeholder)}
/>
{error && {error}
}
@@ -120,10 +151,10 @@ Use {{parameter_name}} syntax for any user-provided values.`;
- Cancel
+ {intl.formatMessage(i18n.cancel)}
- Save Instructions
+ {intl.formatMessage(i18n.save)}
diff --git a/ui/desktop/src/components/recipes/shared/JsonSchemaEditor.tsx b/ui/desktop/src/components/recipes/shared/JsonSchemaEditor.tsx
index b33812e7..265d704d 100644
--- a/ui/desktop/src/components/recipes/shared/JsonSchemaEditor.tsx
+++ b/ui/desktop/src/components/recipes/shared/JsonSchemaEditor.tsx
@@ -1,6 +1,38 @@
import React, { useState } from 'react';
import { Button } from '../../ui/button';
import { useEscapeKey } from '../../../hooks/useEscapeKey';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'jsonSchemaEditor.title',
+ defaultMessage: 'JSON Schema Editor',
+ },
+ label: {
+ id: 'jsonSchemaEditor.label',
+ defaultMessage: 'Response JSON Schema',
+ },
+ insertExample: {
+ id: 'jsonSchemaEditor.insertExample',
+ defaultMessage: 'Insert Example',
+ },
+ description: {
+ id: 'jsonSchemaEditor.description',
+ defaultMessage: "Define the expected structure of the AI's response using JSON Schema format",
+ },
+ invalidJson: {
+ id: 'jsonSchemaEditor.invalidJson',
+ defaultMessage: 'Invalid JSON format',
+ },
+ cancel: {
+ id: 'jsonSchemaEditor.cancel',
+ defaultMessage: 'Cancel',
+ },
+ save: {
+ id: 'jsonSchemaEditor.save',
+ defaultMessage: 'Save Schema',
+ },
+});
interface JsonSchemaEditorProps {
isOpen: boolean;
@@ -17,6 +49,7 @@ export default function JsonSchemaEditor({
onChange,
error,
}: JsonSchemaEditorProps) {
+ const intl = useIntl();
const [localValue, setLocalValue] = useState(value);
const [localError, setLocalError] = useState('');
@@ -35,7 +68,7 @@ export default function JsonSchemaEditor({
JSON.parse(localValue.trim());
setLocalError('');
} catch {
- setLocalError('Invalid JSON format');
+ setLocalError(intl.formatMessage(i18n.invalidJson));
return;
}
}
@@ -94,7 +127,7 @@ export default function JsonSchemaEditor({
>
-
JSON Schema Editor
+
{intl.formatMessage(i18n.title)}
- Response JSON Schema
+ {intl.formatMessage(i18n.label)}
- Insert Example
+ {intl.formatMessage(i18n.insertExample)}
- Define the expected structure of the AI's response using JSON Schema format
+ {intl.formatMessage(i18n.description)}
@@ -154,10 +187,10 @@ export default function JsonSchemaEditor({
- Cancel
+ {intl.formatMessage(i18n.cancel)}
- Save Schema
+ {intl.formatMessage(i18n.save)}
diff --git a/ui/desktop/src/components/recipes/shared/KeyValueEditor.tsx b/ui/desktop/src/components/recipes/shared/KeyValueEditor.tsx
index 053127b5..92ac7c10 100644
--- a/ui/desktop/src/components/recipes/shared/KeyValueEditor.tsx
+++ b/ui/desktop/src/components/recipes/shared/KeyValueEditor.tsx
@@ -1,6 +1,26 @@
import React, { useState } from 'react';
import { Plus, Trash2 } from 'lucide-react';
import { Button } from '../../ui/button';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ addValue: {
+ id: 'keyValueEditor.addValue',
+ defaultMessage: 'Add pre-configured value',
+ },
+ removeValue: {
+ id: 'keyValueEditor.removeValue',
+ defaultMessage: 'Remove pre-configured value {key}',
+ },
+ defaultKeyPlaceholder: {
+ id: 'keyValueEditor.defaultKeyPlaceholder',
+ defaultMessage: 'Parameter name...',
+ },
+ defaultValuePlaceholder: {
+ id: 'keyValueEditor.defaultValuePlaceholder',
+ defaultMessage: 'Parameter value...',
+ },
+});
interface KeyValueEditorProps {
values: Record
;
@@ -12,9 +32,10 @@ interface KeyValueEditorProps {
export default function KeyValueEditor({
values,
onChange,
- keyPlaceholder = 'Parameter name...',
- valuePlaceholder = 'Parameter value...',
+ keyPlaceholder,
+ valuePlaceholder,
}: KeyValueEditorProps) {
+ const intl = useIntl();
const [newKey, setNewKey] = useState('');
const [newValue, setNewValue] = useState('');
@@ -47,7 +68,7 @@ export default function KeyValueEditor({
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
onKeyDown={handleKeyDown}
- placeholder={keyPlaceholder}
+ placeholder={keyPlaceholder || intl.formatMessage(i18n.defaultKeyPlaceholder)}
className="flex-1 px-3 py-2 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring text-sm"
/>
setNewValue(e.target.value)}
onKeyDown={handleKeyDown}
- placeholder={valuePlaceholder}
+ placeholder={valuePlaceholder || intl.formatMessage(i18n.defaultValuePlaceholder)}
className="flex-1 px-3 py-2 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring text-sm"
/>
@@ -89,8 +110,8 @@ export default function KeyValueEditor({
variant="ghost"
size="sm"
className="p-1 hover:bg-background-danger/10 hover:text-text-danger"
- aria-label={`Remove pre-configured value ${key}`}
- title={`Remove pre-configured value ${key}`}
+ aria-label={intl.formatMessage(i18n.removeValue, { key })}
+ title={intl.formatMessage(i18n.removeValue, { key })}
>
diff --git a/ui/desktop/src/components/recipes/shared/RecipeExtensionSelector.tsx b/ui/desktop/src/components/recipes/shared/RecipeExtensionSelector.tsx
index a311c658..381ad420 100644
--- a/ui/desktop/src/components/recipes/shared/RecipeExtensionSelector.tsx
+++ b/ui/desktop/src/components/recipes/shared/RecipeExtensionSelector.tsx
@@ -4,6 +4,34 @@ import { useConfig } from '../../ConfigContext';
import { Input } from '../../ui/input';
import { Switch } from '../../ui/switch';
import { formatExtensionName } from '../../settings/extensions/subcomponents/ExtensionList';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ label: {
+ id: 'recipeExtensionSelector.label',
+ defaultMessage: 'Extensions (Optional)',
+ },
+ description: {
+ id: 'recipeExtensionSelector.description',
+ defaultMessage: 'Select which extensions should be available when running this recipe. Leave empty to use default extensions.',
+ },
+ searchPlaceholder: {
+ id: 'recipeExtensionSelector.searchPlaceholder',
+ defaultMessage: 'Search extensions...',
+ },
+ extensionsSelected: {
+ id: 'recipeExtensionSelector.extensionsSelected',
+ defaultMessage: '{count, plural, one {# extension} other {# extensions}} selected',
+ },
+ noExtensionsFound: {
+ id: 'recipeExtensionSelector.noExtensionsFound',
+ defaultMessage: 'No extensions found',
+ },
+ noExtensionsAvailable: {
+ id: 'recipeExtensionSelector.noExtensionsAvailable',
+ defaultMessage: 'No extensions available',
+ },
+});
interface RecipeExtensionSelectorProps {
selectedExtensions: ExtensionConfig[];
@@ -14,6 +42,7 @@ export const RecipeExtensionSelector = ({
selectedExtensions,
onExtensionsChange,
}: RecipeExtensionSelectorProps) => {
+ const intl = useIntl();
const { extensionsList: allExtensions } = useConfig();
const [searchQuery, setSearchQuery] = useState('');
@@ -65,30 +94,29 @@ export const RecipeExtensionSelector = ({
- Extensions (Optional)
+ {intl.formatMessage(i18n.label)}
- Select which extensions should be available when running this recipe. Leave empty to use
- default extensions.
+ {intl.formatMessage(i18n.description)}
setSearchQuery(e.target.value)}
className="mb-3"
/>
- {activeCount} extension{activeCount !== 1 ? 's' : ''} selected
+ {intl.formatMessage(i18n.extensionsSelected, { count: activeCount })}
{sortedExtensions.length === 0 ? (
- {searchQuery ? 'No extensions found' : 'No extensions available'}
+ {searchQuery ? intl.formatMessage(i18n.noExtensionsFound) : intl.formatMessage(i18n.noExtensionsAvailable)}
) : (
sortedExtensions.map((ext) => {
diff --git a/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx b/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx
index b5351c14..af2a3704 100644
--- a/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx
+++ b/ui/desktop/src/components/recipes/shared/RecipeFormFields.tsx
@@ -2,6 +2,90 @@ import React, { useState } from 'react';
import { Parameter } from '../../../recipe';
import { ChevronDown } from 'lucide-react';
import { ExtensionConfig } from '../../../api';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ titleLabel: {
+ id: 'recipeFormFields.titleLabel',
+ defaultMessage: 'Title',
+ },
+ titlePlaceholder: {
+ id: 'recipeFormFields.titlePlaceholder',
+ defaultMessage: 'Recipe title',
+ },
+ descriptionLabel: {
+ id: 'recipeFormFields.descriptionLabel',
+ defaultMessage: 'Description',
+ },
+ descriptionPlaceholder: {
+ id: 'recipeFormFields.descriptionPlaceholder',
+ defaultMessage: 'Brief description of what this recipe does',
+ },
+ instructionsLabel: {
+ id: 'recipeFormFields.instructionsLabel',
+ defaultMessage: 'Instructions',
+ },
+ openEditor: {
+ id: 'recipeFormFields.openEditor',
+ defaultMessage: 'Open Editor',
+ },
+ instructionsPlaceholder: {
+ id: 'recipeFormFields.instructionsPlaceholder',
+ defaultMessage: 'Detailed instructions for the AI, hidden from the user',
+ },
+ templateVarHint: {
+ id: 'recipeFormFields.templateVarHint',
+ defaultMessage: "Use '{{parameter_name}}' to define parameters that can be filled in when running the recipe.",
+ },
+ initialPrompt: {
+ id: 'recipeFormFields.initialPrompt',
+ defaultMessage: 'Initial Prompt',
+ },
+ promptOptionalHint: {
+ id: 'recipeFormFields.promptOptionalHint',
+ defaultMessage: '(Optional - Instructions or Prompt are required)',
+ },
+ promptPlaceholder: {
+ id: 'recipeFormFields.promptPlaceholder',
+ defaultMessage: 'Pre-filled prompt when the recipe starts',
+ },
+ advancedOptions: {
+ id: 'recipeFormFields.advancedOptions',
+ defaultMessage: 'Advanced Options',
+ },
+ advancedOptionsHint: {
+ id: 'recipeFormFields.advancedOptionsHint',
+ defaultMessage: 'Activities, parameters, model, extensions, response schema, subrecipes',
+ },
+ parametersLabel: {
+ id: 'recipeFormFields.parametersLabel',
+ defaultMessage: 'Parameters',
+ },
+ parametersDescription: {
+ id: 'recipeFormFields.parametersDescription',
+ defaultMessage: "Parameters will be automatically detected from '{{parameter_name}}' syntax in instructions/prompt/activities or you can manually add them below.",
+ },
+ parameterNamePlaceholder: {
+ id: 'recipeFormFields.parameterNamePlaceholder',
+ defaultMessage: 'Enter parameter name...',
+ },
+ addParameter: {
+ id: 'recipeFormFields.addParameter',
+ defaultMessage: 'Add parameter',
+ },
+ enterValueFor: {
+ id: 'recipeFormFields.enterValueFor',
+ defaultMessage: 'Enter value for {key}',
+ },
+ responseJsonSchema: {
+ id: 'recipeFormFields.responseJsonSchema',
+ defaultMessage: 'Response JSON Schema',
+ },
+ responseJsonSchemaDescription: {
+ id: 'recipeFormFields.responseJsonSchemaDescription',
+ defaultMessage: "Define the expected structure of the AI's response using JSON Schema format",
+ },
+});
import ParameterInput from '../../parameter/ParameterInput';
import RecipeActivityEditor from '../RecipeActivityEditor';
@@ -59,6 +143,7 @@ export function RecipeFormFields({
onPromptChange,
onJsonSchemaChange,
}: RecipeFormFieldsProps) {
+ const intl = useIntl();
const [showJsonSchemaEditor, setShowJsonSchemaEditor] = useState(false);
const [showInstructionsEditor, setShowInstructionsEditor] = useState(false);
const [newParameterName, setNewParameterName] = useState('');
@@ -87,12 +172,12 @@ export function RecipeFormFields({
return allVars.map((key: string) => ({
key,
- description: `Enter value for ${key}`,
+ description: intl.formatMessage(i18n.enterValueFor, { key }),
requirement: 'required' as const,
input_type: 'string' as const,
}));
},
- []
+ [intl]
);
// Function to update parameters based on current field values
@@ -173,7 +258,7 @@ export function RecipeFormFields({
htmlFor="recipe-title"
className="block text-sm font-medium text-text-primary mb-2"
>
- Title
*
+ {intl.formatMessage(i18n.titleLabel)}
*
0 ? 'border-red-500' : 'border-border-primary'
}`}
- placeholder="Recipe title"
+ placeholder={intl.formatMessage(i18n.titlePlaceholder)}
data-testid="title-input"
/>
{field.state.meta.errors.length > 0 && (
@@ -205,7 +290,7 @@ export function RecipeFormFields({
htmlFor="recipe-description"
className="block text-sm font-medium text-text-primary mb-2"
>
- Description
*
+ {intl.formatMessage(i18n.descriptionLabel)}
*
0 ? 'border-red-500' : 'border-border-primary'
}`}
- placeholder="Brief description of what this recipe does"
+ placeholder={intl.formatMessage(i18n.descriptionPlaceholder)}
data-testid="description-input"
/>
{field.state.meta.errors.length > 0 && (
@@ -238,7 +323,7 @@ export function RecipeFormFields({
htmlFor="recipe-instructions"
className="block text-sm font-medium text-text-primary"
>
- Instructions
*
+ {intl.formatMessage(i18n.instructionsLabel)}
*
- Open Editor
+ {intl.formatMessage(i18n.openEditor)}
0 ? 'border-red-500' : 'border-border-primary'
}`}
- placeholder="Detailed instructions for the AI, hidden from the user"
+ placeholder={intl.formatMessage(i18n.instructionsPlaceholder)}
rows={8}
data-testid="instructions-input"
/>
- Use {`{{parameter_name}}`} to define parameters that can be filled in when running the
- recipe.
+ {intl.formatMessage(i18n.templateVarHint)}
{field.state.meta.errors.length > 0 && (
{field.state.meta.errors[0]}
@@ -300,10 +384,10 @@ export function RecipeFormFields({
htmlFor="recipe-prompt"
className="block text-sm font-medium text-text-primary mb-2"
>
- Initial Prompt
+ {intl.formatMessage(i18n.initialPrompt)}
- (Optional - Instructions or Prompt are required)
+ {intl.formatMessage(i18n.promptOptionalHint)}
@@ -333,9 +417,9 @@ export function RecipeFormFields({
advancedOpen ? 'rotate-0' : '-rotate-90'
}`}
/>
- Advanced Options
+ {intl.formatMessage(i18n.advancedOptions)}
- Activities, parameters, model, extensions, response schema, subrecipes
+ {intl.formatMessage(i18n.advancedOptionsHint)}
@@ -360,7 +444,7 @@ export function RecipeFormFields({
if (newParameterName.trim()) {
const newParam: Parameter = {
key: newParameterName.trim(),
- description: `Enter value for ${newParameterName.trim()}`,
+ description: intl.formatMessage(i18n.enterValueFor, { key: newParameterName.trim() }),
input_type: 'string',
requirement: 'required',
};
@@ -410,11 +494,10 @@ export function RecipeFormFields({
return (
- Parameters
+ {intl.formatMessage(i18n.parametersLabel)}
- Parameters will be automatically detected from {`{{parameter_name}}`} syntax in
- instructions/prompt/activities or you can manually add them below.
+ {intl.formatMessage(i18n.parametersDescription)}
{/* Add Parameter Input - Always Visible */}
@@ -424,7 +507,7 @@ export function RecipeFormFields({
value={newParameterName}
onChange={(e) => setNewParameterName(e.target.value)}
onKeyDown={handleKeyDown}
- placeholder="Enter parameter name..."
+ placeholder={intl.formatMessage(i18n.parameterNamePlaceholder)}
className="flex-1 px-3 py-2 border border-border-primary rounded-lg bg-background-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
/>
- Add parameter
+ {intl.formatMessage(i18n.addParameter)}
@@ -504,10 +587,10 @@ export function RecipeFormFields({
{(field: FormFieldApi) => (
- Response JSON Schema
+ {intl.formatMessage(i18n.responseJsonSchema)}
- Define the expected structure of the AI's response using JSON Schema format
+ {intl.formatMessage(i18n.responseJsonSchemaDescription)}
- Open Editor
+ {intl.formatMessage(i18n.openEditor)}
diff --git a/ui/desktop/src/components/recipes/shared/RecipeModelSelector.tsx b/ui/desktop/src/components/recipes/shared/RecipeModelSelector.tsx
index fe60eb17..2a0c310e 100644
--- a/ui/desktop/src/components/recipes/shared/RecipeModelSelector.tsx
+++ b/ui/desktop/src/components/recipes/shared/RecipeModelSelector.tsx
@@ -3,6 +3,58 @@ import { Select } from '../../ui/Select';
import { Input } from '../../ui/input';
import { useConfig } from '../../ConfigContext';
import { fetchModelsForProviders } from '../../settings/models/modelInterface';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ fetchError: {
+ id: 'recipeModelSelector.fetchError',
+ defaultMessage: 'Failed to fetch models. Please try again later.',
+ },
+ providerLabel: {
+ id: 'recipeModelSelector.providerLabel',
+ defaultMessage: 'Provider (Optional)',
+ },
+ providerHint: {
+ id: 'recipeModelSelector.providerHint',
+ defaultMessage: 'Leave empty to use the default provider configured in settings',
+ },
+ selectProvider: {
+ id: 'recipeModelSelector.selectProvider',
+ defaultMessage: 'Select provider',
+ },
+ useDefaultProvider: {
+ id: 'recipeModelSelector.useDefaultProvider',
+ defaultMessage: 'Use default provider',
+ },
+ enterModelNotListed: {
+ id: 'recipeModelSelector.enterModelNotListed',
+ defaultMessage: 'Enter a model not listed...',
+ },
+ modelLabel: {
+ id: 'recipeModelSelector.modelLabel',
+ defaultMessage: 'Model (Optional)',
+ },
+ backToModelList: {
+ id: 'recipeModelSelector.backToModelList',
+ defaultMessage: 'Back to model list',
+ },
+ modelHint: {
+ id: 'recipeModelSelector.modelHint',
+ defaultMessage: 'Leave empty to use the default model for the selected provider',
+ },
+ enterCustomModel: {
+ id: 'recipeModelSelector.enterCustomModel',
+ defaultMessage: 'Enter custom model name',
+ },
+ loadingModels: {
+ id: 'recipeModelSelector.loadingModels',
+ defaultMessage: 'Loading models…',
+ },
+ selectModel: {
+ id: 'recipeModelSelector.selectModel',
+ defaultMessage: 'Select a model',
+ },
+});
interface RecipeModelSelectorProps {
selectedProvider?: string;
@@ -17,6 +69,7 @@ export const RecipeModelSelector = ({
onProviderChange,
onModelChange,
}: RecipeModelSelectorProps) => {
+ const intl = useIntl();
const { getProviders } = useConfig();
const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]);
const [modelOptions, setModelOptions] = useState<
@@ -34,7 +87,7 @@ export const RecipeModelSelector = ({
const activeProviders = providersResponse.filter((provider) => provider.is_configured);
setProviderOptions([
- { value: '', label: 'Use default provider' },
+ { value: '', label: intl.formatMessage(i18n.useDefaultProvider) },
...activeProviders.map(({ metadata, name }) => ({
value: name,
label: metadata.display_name,
@@ -62,7 +115,7 @@ export const RecipeModelSelector = ({
options.push({
value: `__custom__:${p.name}`,
- label: 'Enter a model not listed...',
+ label: intl.formatMessage(i18n.enterModelNotListed),
provider: p.name,
});
@@ -74,12 +127,12 @@ export const RecipeModelSelector = ({
setModelOptions(groupedOptions);
} catch (error) {
console.error('Failed to load providers:', error);
- setFetchError('Failed to fetch models. Please try again later.');
+ setFetchError(intl.formatMessage(i18n.fetchError));
} finally {
setLoadingModels(false);
}
})();
- }, [getProviders]);
+ }, [getProviders, intl]);
useEffect(() => {
if (!loadingModels && selectedModel && selectedProvider) {
@@ -131,10 +184,10 @@ export const RecipeModelSelector = ({
)}
- Provider (Optional)
+ {intl.formatMessage(i18n.providerLabel)}
- Leave empty to use the default provider configured in settings
+ {intl.formatMessage(i18n.providerHint)}
opt.value === '') || null
}
onChange={handleProviderChange}
- placeholder="Select provider"
+ placeholder={intl.formatMessage(i18n.selectProvider)}
isClearable
/>
- Model (Optional)
+ {intl.formatMessage(i18n.modelLabel)}
{isCustomModel && (
{
@@ -161,17 +214,17 @@ export const RecipeModelSelector = ({
className="text-xs text-textSubtle hover:underline"
type="button"
>
- Back to model list
+ {intl.formatMessage(i18n.backToModelList)}
)}
- Leave empty to use the default model for the selected provider
+ {intl.formatMessage(i18n.modelHint)}
{isCustomModel ? (
onModelChange(e.target.value || undefined)}
/>
@@ -180,13 +233,13 @@ export const RecipeModelSelector = ({
options={loadingModels ? [] : filteredModelOptions}
value={
loadingModels
- ? { value: '', label: 'Loading models…', isDisabled: true }
+ ? { value: '', label: intl.formatMessage(i18n.loadingModels), isDisabled: true }
: selectedModel
? { value: selectedModel, label: selectedModel }
: null
}
onChange={handleModelChange}
- placeholder="Select a model"
+ placeholder={intl.formatMessage(i18n.selectModel)}
isClearable
isDisabled={loadingModels}
/>
diff --git a/ui/desktop/src/components/recipes/shared/RecipeNameField.tsx b/ui/desktop/src/components/recipes/shared/RecipeNameField.tsx
index bd75cfed..33dd2fcd 100644
--- a/ui/desktop/src/components/recipes/shared/RecipeNameField.tsx
+++ b/ui/desktop/src/components/recipes/shared/RecipeNameField.tsx
@@ -1,4 +1,16 @@
import { recipeNameSchema, RECIPE_NAME_PLACEHOLDER } from './recipeNameUtils';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ defaultLabel: {
+ id: 'recipeNameField.defaultLabel',
+ defaultMessage: 'Recipe Name',
+ },
+ formatHint: {
+ id: 'recipeNameField.formatHint',
+ defaultMessage: 'Will be automatically formatted (lowercase, dashes for spaces)',
+ },
+});
interface RecipeNameFieldProps {
id: string;
@@ -17,14 +29,16 @@ export function RecipeNameField({
onChange,
onBlur,
errors,
- label = 'Recipe Name',
+ label,
required = true,
disabled = false,
}: RecipeNameFieldProps) {
+ const intl = useIntl();
+ const displayLabel = label || intl.formatMessage(i18n.defaultLabel);
return (
- {label} {required && * }
+ {displayLabel} {required && * }
- Will be automatically formatted (lowercase, dashes for spaces)
+ {intl.formatMessage(i18n.formatHint)}
{errors.length > 0 &&
{errors[0]}
}
diff --git a/ui/desktop/src/components/recipes/shared/SubRecipeEditor.tsx b/ui/desktop/src/components/recipes/shared/SubRecipeEditor.tsx
index dbfc6e0c..844ce7b1 100644
--- a/ui/desktop/src/components/recipes/shared/SubRecipeEditor.tsx
+++ b/ui/desktop/src/components/recipes/shared/SubRecipeEditor.tsx
@@ -5,6 +5,50 @@ import { SubRecipeFormData } from './recipeFormSchema';
import SubRecipeModal from './SubRecipeModal';
import CreateSubRecipeInline from './CreateSubRecipeInline';
import { toastError } from '../../../toasts';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ label: {
+ id: 'subRecipeEditor.label',
+ defaultMessage: 'Subrecipes',
+ },
+ createNew: {
+ id: 'subRecipeEditor.createNew',
+ defaultMessage: 'Create New Subrecipe',
+ },
+ addExisting: {
+ id: 'subRecipeEditor.addExisting',
+ defaultMessage: 'Add Existing',
+ },
+ description: {
+ id: 'subRecipeEditor.description',
+ defaultMessage: 'Subrecipes are recipes that can be called as tools during execution. They enable multi-step workflows and reusable components.',
+ },
+ sequential: {
+ id: 'subRecipeEditor.sequential',
+ defaultMessage: 'Sequential',
+ },
+ preconfiguredValues: {
+ id: 'subRecipeEditor.preconfiguredValues',
+ defaultMessage: 'Pre-configured values:',
+ },
+ editSubrecipe: {
+ id: 'subRecipeEditor.editSubrecipe',
+ defaultMessage: 'Edit subrecipe {name}',
+ },
+ deleteSubrecipe: {
+ id: 'subRecipeEditor.deleteSubrecipe',
+ defaultMessage: 'Delete subrecipe {name}',
+ },
+ duplicateName: {
+ id: 'subRecipeEditor.duplicateName',
+ defaultMessage: 'Duplicate Name',
+ },
+ duplicateNameMsg: {
+ id: 'subRecipeEditor.duplicateNameMsg',
+ defaultMessage: 'A subrecipe named "{name}" already exists. Please use a unique name.',
+ },
+});
interface SubRecipeEditorProps {
subRecipes: SubRecipeFormData[];
@@ -12,6 +56,7 @@ interface SubRecipeEditorProps {
}
export default function SubRecipeEditor({ subRecipes, onChange }: SubRecipeEditorProps) {
+ const intl = useIntl();
const [showModal, setShowModal] = useState(false);
const [editingSubRecipe, setEditingSubRecipe] = useState
(null);
const [editingIndex, setEditingIndex] = useState(null);
@@ -44,8 +89,8 @@ export default function SubRecipeEditor({ subRecipes, onChange }: SubRecipeEdito
);
if (isDuplicate) {
toastError({
- title: 'Duplicate Name',
- msg: `A subrecipe named "${subRecipe.name}" already exists. Please use a unique name.`,
+ title: intl.formatMessage(i18n.duplicateName),
+ msg: intl.formatMessage(i18n.duplicateNameMsg, { name: subRecipe.name }),
});
return false;
}
@@ -62,8 +107,8 @@ export default function SubRecipeEditor({ subRecipes, onChange }: SubRecipeEdito
const handleSubRecipeSaved = (subRecipe: SubRecipeFormData) => {
if (subRecipes.some((sr) => sr.name === subRecipe.name)) {
toastError({
- title: 'Duplicate Name',
- msg: `A subrecipe named "${subRecipe.name}" already exists. Please use a unique name.`,
+ title: intl.formatMessage(i18n.duplicateName),
+ msg: intl.formatMessage(i18n.duplicateNameMsg, { name: subRecipe.name }),
});
return;
}
@@ -73,7 +118,7 @@ export default function SubRecipeEditor({ subRecipes, onChange }: SubRecipeEdito
return (
-
Subrecipes
+
{intl.formatMessage(i18n.label)}
- Create New Subrecipe
+ {intl.formatMessage(i18n.createNew)}
- Add Existing
+ {intl.formatMessage(i18n.addExisting)}
- Subrecipes are recipes that can be called as tools during execution. They enable multi-step
- workflows and reusable components.
+ {intl.formatMessage(i18n.description)}
{subRecipes.length > 0 && (
@@ -116,7 +160,7 @@ export default function SubRecipeEditor({ subRecipes, onChange }: SubRecipeEdito
{subRecipe.name}
{subRecipe.sequential_when_repeated && (
- Sequential
+ {intl.formatMessage(i18n.sequential)}
)}
@@ -126,7 +170,7 @@ export default function SubRecipeEditor({ subRecipes, onChange }: SubRecipeEdito
)}
{subRecipe.values && Object.keys(subRecipe.values).length > 0 && (
-
Pre-configured values:
+
{intl.formatMessage(i18n.preconfiguredValues)}
{Object.entries(subRecipe.values).map(([key, value]) => (
@@ -160,8 +204,8 @@ export default function SubRecipeEditor({ subRecipes, onChange }: SubRecipeEdito
variant="ghost"
size="sm"
className="p-2 hover:bg-background-danger/10 hover:text-text-danger"
- aria-label={`Delete subrecipe ${subRecipe.name}`}
- title={`Delete subrecipe ${subRecipe.name}`}
+ aria-label={intl.formatMessage(i18n.deleteSubrecipe, { name: subRecipe.name })}
+ title={intl.formatMessage(i18n.deleteSubrecipe, { name: subRecipe.name })}
>
diff --git a/ui/desktop/src/components/recipes/shared/SubRecipeModal.tsx b/ui/desktop/src/components/recipes/shared/SubRecipeModal.tsx
index eaed18fd..fc15d0ad 100644
--- a/ui/desktop/src/components/recipes/shared/SubRecipeModal.tsx
+++ b/ui/desktop/src/components/recipes/shared/SubRecipeModal.tsx
@@ -5,6 +5,94 @@ import { SubRecipeFormData } from './recipeFormSchema';
import { useEscapeKey } from '../../../hooks/useEscapeKey';
import KeyValueEditor from './KeyValueEditor';
import { toastError } from '../../../toasts';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ configureTitle: {
+ id: 'subRecipeModal.configureTitle',
+ defaultMessage: 'Configure Subrecipe',
+ },
+ addTitle: {
+ id: 'subRecipeModal.addTitle',
+ defaultMessage: 'Add Subrecipe',
+ },
+ subtitle: {
+ id: 'subRecipeModal.subtitle',
+ defaultMessage: 'Configure a subrecipe that can be called as a tool during recipe execution',
+ },
+ closeModal: {
+ id: 'subRecipeModal.closeModal',
+ defaultMessage: 'Close subrecipe modal',
+ },
+ nameLabel: {
+ id: 'subRecipeModal.nameLabel',
+ defaultMessage: 'Name',
+ },
+ namePlaceholder: {
+ id: 'subRecipeModal.namePlaceholder',
+ defaultMessage: 'e.g., security_scan',
+ },
+ nameHint: {
+ id: 'subRecipeModal.nameHint',
+ defaultMessage: 'Unique identifier used to generate the tool name',
+ },
+ pathLabel: {
+ id: 'subRecipeModal.pathLabel',
+ defaultMessage: 'Path',
+ },
+ pathPlaceholder: {
+ id: 'subRecipeModal.pathPlaceholder',
+ defaultMessage: 'e.g., ./subrecipes/security-analysis.yaml',
+ },
+ browse: {
+ id: 'subRecipeModal.browse',
+ defaultMessage: 'Browse',
+ },
+ pathHint: {
+ id: 'subRecipeModal.pathHint',
+ defaultMessage: 'Browse for an existing recipe file or enter a path manually',
+ },
+ descriptionLabel: {
+ id: 'subRecipeModal.descriptionLabel',
+ defaultMessage: 'Description',
+ },
+ descriptionPlaceholder: {
+ id: 'subRecipeModal.descriptionPlaceholder',
+ defaultMessage: 'Optional description of what this subrecipe does...',
+ },
+ sequentialLabel: {
+ id: 'subRecipeModal.sequentialLabel',
+ defaultMessage: 'Sequential when repeated',
+ },
+ sequentialHint: {
+ id: 'subRecipeModal.sequentialHint',
+ defaultMessage: '(Forces sequential execution of multiple subrecipe instances)',
+ },
+ preconfiguredValues: {
+ id: 'subRecipeModal.preconfiguredValues',
+ defaultMessage: 'Pre-configured Values',
+ },
+ preconfiguredValuesHint: {
+ id: 'subRecipeModal.preconfiguredValuesHint',
+ defaultMessage: 'Optional parameter values that are always passed to the subrecipe',
+ },
+ cancel: {
+ id: 'subRecipeModal.cancel',
+ defaultMessage: 'Cancel',
+ },
+ apply: {
+ id: 'subRecipeModal.apply',
+ defaultMessage: 'Apply',
+ },
+ invalidFile: {
+ id: 'subRecipeModal.invalidFile',
+ defaultMessage: 'Invalid File',
+ },
+ invalidFileMsg: {
+ id: 'subRecipeModal.invalidFileMsg',
+ defaultMessage: 'Please select a YAML file (.yaml or .yml).',
+ },
+});
interface SubRecipeModalProps {
isOpen: boolean;
@@ -19,6 +107,7 @@ export default function SubRecipeModal({
onSave,
subRecipe,
}: SubRecipeModalProps) {
+ const intl = useIntl();
const [name, setName] = useState('');
const [path, setPath] = useState('');
const [description, setDescription] = useState('');
@@ -69,8 +158,8 @@ export default function SubRecipeModal({
if (selectedPath) {
if (!selectedPath.endsWith('.yaml') && !selectedPath.endsWith('.yml')) {
toastError({
- title: 'Invalid File',
- msg: 'Please select a YAML file (.yaml or .yml).',
+ title: intl.formatMessage(i18n.invalidFile),
+ msg: intl.formatMessage(i18n.invalidFileMsg),
});
return;
}
@@ -90,10 +179,10 @@ export default function SubRecipeModal({
- {subRecipe ? 'Configure Subrecipe' : 'Add Subrecipe'}
+ {subRecipe ? intl.formatMessage(i18n.configureTitle) : intl.formatMessage(i18n.addTitle)}
- Configure a subrecipe that can be called as a tool during recipe execution
+ {intl.formatMessage(i18n.subtitle)}
@@ -115,7 +204,7 @@ export default function SubRecipeModal({
htmlFor="subrecipe-name"
className="block text-sm font-medium text-text-standard mb-2"
>
- Name
*
+ {intl.formatMessage(i18n.nameLabel)}
*
setName(e.target.value)}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring"
- placeholder="e.g., security_scan"
+ placeholder={intl.formatMessage(i18n.namePlaceholder)}
/>
- Unique identifier used to generate the tool name
+ {intl.formatMessage(i18n.nameHint)}
@@ -136,7 +225,7 @@ export default function SubRecipeModal({
htmlFor="subrecipe-path"
className="block text-sm font-medium text-text-standard mb-2"
>
- Path *
+ {intl.formatMessage(i18n.pathLabel)} *
setPath(e.target.value)}
className="flex-1 p-3 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring"
- placeholder="e.g., ./subrecipes/security-analysis.yaml"
+ placeholder={intl.formatMessage(i18n.pathPlaceholder)}
/>
- Browse
+ {intl.formatMessage(i18n.browse)}
- Browse for an existing recipe file or enter a path manually
+ {intl.formatMessage(i18n.pathHint)}
@@ -168,14 +257,14 @@ export default function SubRecipeModal({
htmlFor="subrecipe-description"
className="block text-sm font-medium text-text-standard mb-2"
>
- Description
+ {intl.formatMessage(i18n.descriptionLabel)}
setDescription(e.target.value)}
className="w-full p-3 border border-border-subtle rounded-lg bg-background-primary text-text-standard focus:outline-none focus:ring-2 focus:ring-ring resize-none"
- placeholder="Optional description of what this subrecipe does..."
+ placeholder={intl.formatMessage(i18n.descriptionPlaceholder)}
rows={3}
/>
@@ -190,20 +279,20 @@ export default function SubRecipeModal({
className="w-4 h-4 border-border-subtle rounded focus:ring-2 focus:ring-ring"
/>
- Sequential when repeated
+ {intl.formatMessage(i18n.sequentialLabel)}
- (Forces sequential execution of multiple subrecipe instances)
+ {intl.formatMessage(i18n.sequentialHint)}
{/* Values Section */}
- Pre-configured Values
+ {intl.formatMessage(i18n.preconfiguredValues)}
- Optional parameter values that are always passed to the subrecipe
+ {intl.formatMessage(i18n.preconfiguredValuesHint)}
@@ -212,10 +301,10 @@ export default function SubRecipeModal({
{/* Footer */}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
- {subRecipe ? 'Apply' : 'Add Subrecipe'}
+ {subRecipe ? intl.formatMessage(i18n.apply) : intl.formatMessage(i18n.addTitle)}
diff --git a/ui/desktop/src/components/recipes/shared/__tests__/RecipeActivityEditor.test.tsx b/ui/desktop/src/components/recipes/shared/__tests__/RecipeActivityEditor.test.tsx
index fafa57e7..bc508f48 100644
--- a/ui/desktop/src/components/recipes/shared/__tests__/RecipeActivityEditor.test.tsx
+++ b/ui/desktop/src/components/recipes/shared/__tests__/RecipeActivityEditor.test.tsx
@@ -1,8 +1,12 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, screen } from '@testing-library/react';
+import { render, type RenderOptions, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import RecipeActivityEditor from '../../RecipeActivityEditor';
+import { IntlTestWrapper } from '../../../../i18n/test-utils';
+
+const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) =>
+ render(ui, { wrapper: IntlTestWrapper, ...options });
describe('RecipeActivityEditor', () => {
const mockOnChange = vi.fn();
@@ -14,24 +18,24 @@ describe('RecipeActivityEditor', () => {
describe('Basic Rendering', () => {
it('renders without crashing', () => {
- render( );
+ renderWithIntl( );
expect(screen.getByText('Activities')).toBeInTheDocument();
});
it('displays the activities label', () => {
- render( );
+ renderWithIntl( );
expect(screen.getByText('Activities')).toBeInTheDocument();
});
it('shows helper text', () => {
- render( );
+ renderWithIntl( );
expect(screen.getByText(/top-line prompts and activity buttons/)).toBeInTheDocument();
});
});
describe('Empty State', () => {
it('shows message input when no activities', () => {
- render( );
+ renderWithIntl( );
expect(screen.getByText('Message')).toBeInTheDocument();
expect(
screen.getByPlaceholderText(/Enter a user facing introduction message/)
@@ -42,7 +46,7 @@ describe('RecipeActivityEditor', () => {
describe('With Activities', () => {
it('displays existing activities as visual boxes', () => {
const activities = ['message: Hello World', 'button: Click me', 'action: Do something'];
- render( );
+ renderWithIntl( );
const messageTextarea = screen.getByPlaceholderText(
/Enter a user facing introduction message/
@@ -59,7 +63,7 @@ describe('RecipeActivityEditor', () => {
it('truncates long activity text in boxes', () => {
const longActivity = 'button: ' + 'a'.repeat(150);
const activities = [longActivity];
- render( );
+ renderWithIntl( );
expect(screen.getByText(/button: a+\.\.\./)).toBeInTheDocument();
@@ -68,7 +72,7 @@ describe('RecipeActivityEditor', () => {
});
it('handles empty activities array', () => {
- render( );
+ renderWithIntl( );
expect(screen.getByText('Activities')).toBeInTheDocument();
expect(screen.queryByText('×')).not.toBeInTheDocument();
@@ -77,7 +81,7 @@ describe('RecipeActivityEditor', () => {
it('allows removing activities via remove buttons', async () => {
const user = userEvent.setup();
const activities = ['button: Click me', 'action: Do something'];
- render( );
+ renderWithIntl( );
const removeButtons = screen.getAllByText('×');
await user.click(removeButtons[0]);
@@ -89,7 +93,7 @@ describe('RecipeActivityEditor', () => {
describe('User Interactions', () => {
it('allows typing in message field', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
const messageInput = screen.getByPlaceholderText(/Enter a user facing introduction message/);
await user.type(messageInput, 'Test message');
@@ -99,7 +103,7 @@ describe('RecipeActivityEditor', () => {
it('calls onBlur when provided', async () => {
const user = userEvent.setup();
- render(
+ renderWithIntl(
);
diff --git a/ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx b/ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx
index 669afe47..d941f521 100644
--- a/ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx
+++ b/ui/desktop/src/components/recipes/shared/__tests__/RecipeFormFields.test.tsx
@@ -1,10 +1,14 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, screen } from '@testing-library/react';
+import { render, type RenderOptions, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useForm } from '@tanstack/react-form';
import { RecipeFormFields, extractTemplateVariables } from '../RecipeFormFields';
import { type RecipeFormData } from '../recipeFormSchema';
+import { IntlTestWrapper } from '../../../../i18n/test-utils';
+
+const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) =>
+ render(ui, { wrapper: IntlTestWrapper, ...options });
vi.mock('../../../ConfigContext', () => ({
useConfig: () => ({
@@ -64,13 +68,13 @@ describe('RecipeFormFields', () => {
describe('Basic Rendering', () => {
it('renders the component without crashing', () => {
- render( );
+ renderWithIntl( );
expect(screen.getByLabelText(/title/i)).toBeInTheDocument();
});
it('renders required form fields', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
expect(screen.getByLabelText(/title/i)).toBeInTheDocument();
expect(screen.getByLabelText(/description/i)).toBeInTheDocument();
@@ -87,7 +91,7 @@ describe('RecipeFormFields', () => {
});
it('shows form inputs with proper accessibility', () => {
- render( );
+ renderWithIntl( );
expect(screen.getByRole('textbox', { name: /title/i })).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: /description/i })).toBeInTheDocument();
@@ -99,7 +103,7 @@ describe('RecipeFormFields', () => {
describe('Form Interactions', () => {
it('allows typing in text fields', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
const titleInput = screen.getByRole('textbox', { name: /title/i });
await user.type(titleInput, 'Test Recipe');
@@ -112,7 +116,7 @@ describe('RecipeFormFields', () => {
it('allows typing in textarea fields', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
const instructionsInput = screen.getByRole('textbox', { name: /instructions/i });
await user.type(instructionsInput, 'Do something');
@@ -127,7 +131,7 @@ describe('RecipeFormFields', () => {
describe('Parameter Management', () => {
it('shows parameter input section', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -137,7 +141,7 @@ describe('RecipeFormFields', () => {
it('allows adding parameters manually', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -167,7 +171,7 @@ describe('RecipeFormFields', () => {
prompt: 'Pre-filled Prompt',
};
- render( );
+ renderWithIntl( );
expect(screen.getByDisplayValue('Pre-filled Title')).toBeInTheDocument();
expect(screen.getByDisplayValue('Pre-filled Description')).toBeInTheDocument();
@@ -178,7 +182,7 @@ describe('RecipeFormFields', () => {
describe('Editor Buttons', () => {
it('shows editor buttons for instructions and JSON schema', () => {
- render( );
+ renderWithIntl( );
const editorButtons = screen.getAllByText('Open Editor');
expect(editorButtons.length).toBeGreaterThan(0);
@@ -188,7 +192,7 @@ describe('RecipeFormFields', () => {
describe('Parameter Auto-Detection', () => {
it('has parameter detection functionality', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
const instructionsInput = screen.getByPlaceholderText(
'Detailed instructions for the AI, hidden from the user'
@@ -217,7 +221,7 @@ describe('RecipeFormFields', () => {
it('allows manual parameter addition', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -237,7 +241,7 @@ describe('RecipeFormFields', () => {
it('shows parameter management UI', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -252,7 +256,7 @@ describe('RecipeFormFields', () => {
it('handles activities field for parameter detection', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -300,7 +304,7 @@ describe('RecipeFormFields', () => {
return ;
};
- render( );
+ renderWithIntl( );
const instructionsInput = screen.getByPlaceholderText(
'Detailed instructions for the AI, hidden from the user'
@@ -397,7 +401,7 @@ describe('RecipeFormFields', () => {
return ;
};
- render( );
+ renderWithIntl( );
// Check that parameter names are displayed in code blocks with more specific selectors
const usernameCode = screen.getByText('username').closest('code');
@@ -456,7 +460,7 @@ describe('RecipeFormFields', () => {
it('renders parameter form fields when manually adding parameters', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -568,7 +572,7 @@ describe('RecipeFormFields', () => {
return ;
};
- render( );
+ renderWithIntl( );
// Check that unused indicators are shown
const unusedTexts = screen.getAllByText('Unused');
@@ -645,7 +649,7 @@ describe('RecipeFormFields', () => {
return ;
};
- render( );
+ renderWithIntl( );
// Should have 3 parameters total
const parameterContainers = document.querySelectorAll('.parameter-input');
@@ -675,7 +679,7 @@ describe('RecipeFormFields', () => {
it('shows delete button for parameters', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -708,7 +712,7 @@ describe('RecipeFormFields', () => {
it('supports different parameter input types', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -879,7 +883,7 @@ describe('RecipeFormFields', () => {
describe('Model and Extension Selection', () => {
it('renders model and extension selectors in advanced options', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -913,7 +917,7 @@ describe('RecipeFormFields', () => {
return ;
};
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -945,7 +949,7 @@ describe('RecipeFormFields', () => {
return ;
};
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -968,7 +972,7 @@ describe('RecipeFormFields', () => {
return ;
};
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -999,7 +1003,7 @@ describe('RecipeFormFields', () => {
return ;
};
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -1011,7 +1015,7 @@ describe('RecipeFormFields', () => {
describe('Subrecipes Field', () => {
it('renders the subrecipes section in advanced options', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -1033,7 +1037,7 @@ describe('RecipeFormFields', () => {
],
};
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -1055,7 +1059,7 @@ describe('RecipeFormFields', () => {
],
};
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -1079,7 +1083,7 @@ describe('RecipeFormFields', () => {
],
};
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -1089,7 +1093,7 @@ describe('RecipeFormFields', () => {
it('opens the add existing subrecipe modal on button click', async () => {
const user = userEvent.setup();
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
@@ -1113,7 +1117,7 @@ describe('RecipeFormFields', () => {
],
};
- render( );
+ renderWithIntl( );
await expandAdvancedSection(user);
diff --git a/ui/desktop/src/components/schedule/CronPicker.tsx b/ui/desktop/src/components/schedule/CronPicker.tsx
index 4e164d56..8e9cf406 100644
--- a/ui/desktop/src/components/schedule/CronPicker.tsx
+++ b/ui/desktop/src/components/schedule/CronPicker.tsx
@@ -2,6 +2,42 @@ import React, { useState, useEffect } from 'react';
import cronstrue from 'cronstrue';
import { ScheduledJob } from '../../schedule';
import { errorMessage } from '../../utils/conversionUtils';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ every: { id: 'cronPicker.every', defaultMessage: 'Every' },
+ minute: { id: 'cronPicker.minute', defaultMessage: 'Minute' },
+ hour: { id: 'cronPicker.hour', defaultMessage: 'Hour' },
+ day: { id: 'cronPicker.day', defaultMessage: 'Day' },
+ week: { id: 'cronPicker.week', defaultMessage: 'Week' },
+ month: { id: 'cronPicker.month', defaultMessage: 'Month' },
+ year: { id: 'cronPicker.year', defaultMessage: 'Year' },
+ inMonth: { id: 'cronPicker.inMonth', defaultMessage: 'in' },
+ january: { id: 'cronPicker.january', defaultMessage: 'January' },
+ february: { id: 'cronPicker.february', defaultMessage: 'February' },
+ march: { id: 'cronPicker.march', defaultMessage: 'March' },
+ april: { id: 'cronPicker.april', defaultMessage: 'April' },
+ may: { id: 'cronPicker.may', defaultMessage: 'May' },
+ june: { id: 'cronPicker.june', defaultMessage: 'June' },
+ july: { id: 'cronPicker.july', defaultMessage: 'July' },
+ august: { id: 'cronPicker.august', defaultMessage: 'August' },
+ september: { id: 'cronPicker.september', defaultMessage: 'September' },
+ october: { id: 'cronPicker.october', defaultMessage: 'October' },
+ november: { id: 'cronPicker.november', defaultMessage: 'November' },
+ december: { id: 'cronPicker.december', defaultMessage: 'December' },
+ onDay: { id: 'cronPicker.onDay', defaultMessage: 'on day' },
+ on: { id: 'cronPicker.on', defaultMessage: 'on' },
+ sunday: { id: 'cronPicker.sunday', defaultMessage: 'Sunday' },
+ monday: { id: 'cronPicker.monday', defaultMessage: 'Monday' },
+ tuesday: { id: 'cronPicker.tuesday', defaultMessage: 'Tuesday' },
+ wednesday: { id: 'cronPicker.wednesday', defaultMessage: 'Wednesday' },
+ thursday: { id: 'cronPicker.thursday', defaultMessage: 'Thursday' },
+ friday: { id: 'cronPicker.friday', defaultMessage: 'Friday' },
+ saturday: { id: 'cronPicker.saturday', defaultMessage: 'Saturday' },
+ at: { id: 'cronPicker.at', defaultMessage: 'at' },
+ atMinute: { id: 'cronPicker.atMinute', defaultMessage: 'at minute' },
+ atSecond: { id: 'cronPicker.atSecond', defaultMessage: 'at second' },
+});
type Period = 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';
@@ -79,6 +115,7 @@ const to12Hour = (hour24: number): { hour: number; isPM: boolean } => {
};
export const CronPicker: React.FC = ({ schedule, onChange, isValid }) => {
+ const intl = useIntl();
const [period, setPeriod] = useState('day');
const [second, setSecond] = useState('0');
const [minute, setMinute] = useState('0');
@@ -148,49 +185,49 @@ export const CronPicker: React.FC = ({ schedule, onChange, isVa
return (
- Every
+ {intl.formatMessage(i18n.every)}
setPeriod(e.target.value as Period)}
className={selectClassName}
>
- Minute
- Hour
- Day
- Week
- Month
- Year
+ {intl.formatMessage(i18n.minute)}
+ {intl.formatMessage(i18n.hour)}
+ {intl.formatMessage(i18n.day)}
+ {intl.formatMessage(i18n.week)}
+ {intl.formatMessage(i18n.month)}
+ {intl.formatMessage(i18n.year)}
{period === 'year' && (
- in
+ {intl.formatMessage(i18n.inMonth)}
setMonth(e.target.value)}
className={selectClassName}
>
- January
- February
- March
- April
- May
- June
- July
- August
- September
- October
- November
- December
+ {intl.formatMessage(i18n.january)}
+ {intl.formatMessage(i18n.february)}
+ {intl.formatMessage(i18n.march)}
+ {intl.formatMessage(i18n.april)}
+ {intl.formatMessage(i18n.may)}
+ {intl.formatMessage(i18n.june)}
+ {intl.formatMessage(i18n.july)}
+ {intl.formatMessage(i18n.august)}
+ {intl.formatMessage(i18n.september)}
+ {intl.formatMessage(i18n.october)}
+ {intl.formatMessage(i18n.november)}
+ {intl.formatMessage(i18n.december)}
)}
{(period === 'month' || period === 'year') && (
-
on day
+
{intl.formatMessage(i18n.onDay)}
= ({ schedule, onChange, isVa
{period === 'week' && (
- on
+ {intl.formatMessage(i18n.on)}
setDayOfWeek(e.target.value)}
className={selectClassName}
>
- Sunday
- Monday
- Tuesday
- Wednesday
- Thursday
- Friday
- Saturday
+ {intl.formatMessage(i18n.sunday)}
+ {intl.formatMessage(i18n.monday)}
+ {intl.formatMessage(i18n.tuesday)}
+ {intl.formatMessage(i18n.wednesday)}
+ {intl.formatMessage(i18n.thursday)}
+ {intl.formatMessage(i18n.friday)}
+ {intl.formatMessage(i18n.saturday)}
)}
{(period === 'day' || period === 'week' || period === 'month' || period === 'year') && (
-
at
+
{intl.formatMessage(i18n.at)}
= ({ schedule, onChange, isVa
{period === 'hour' && (
-
at minute
+
{intl.formatMessage(i18n.atMinute)}
= ({ schedule, onChange, isVa
{period === 'minute' && (
-
at second
+
{intl.formatMessage(i18n.atSecond)}
= ({ scheduleId, onNavigateBack }) => {
+ const intl = useIntl();
const [sessions, setSessions] = useState
([]);
const [isLoadingSessions, setIsLoadingSessions] = useState(false);
const [sessionsError, setSessionsError] = useState(null);
@@ -74,7 +132,7 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
}
};
- const fetchSchedule = async (sId: string) => {
+ const fetchSchedule = useCallback(async (sId: string) => {
setIsLoadingSchedule(true);
setScheduleError(null);
try {
@@ -83,21 +141,21 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
if (schedule) {
setScheduleDetails(schedule);
} else {
- setScheduleError('Schedule not found');
+ setScheduleError(intl.formatMessage(i18n.scheduleNotFoundError));
}
} catch (err) {
setScheduleError(errorMessage(err, 'Failed to fetch schedule'));
} finally {
setIsLoadingSchedule(false);
}
- };
+ }, [intl]);
useEffect(() => {
if (scheduleId && !selectedSession) {
fetchSessions(scheduleId);
fetchSchedule(scheduleId);
}
- }, [scheduleId, selectedSession]);
+ }, [scheduleId, selectedSession, fetchSchedule]);
const handleRunNow = async () => {
if (!scheduleId) return;
@@ -106,9 +164,9 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
const newSessionId = await runScheduleNow(scheduleId);
trackScheduleRunNow(true);
if (newSessionId === 'CANCELLED') {
- toastSuccess({ title: 'Job Cancelled', msg: 'The job was cancelled while starting up.' });
+ toastSuccess({ title: intl.formatMessage(i18n.jobCancelled), msg: intl.formatMessage(i18n.jobCancelledMsg) });
} else {
- toastSuccess({ title: 'Schedule Triggered', msg: `New session: ${newSessionId}` });
+ toastSuccess({ title: intl.formatMessage(i18n.scheduleTriggered), msg: intl.formatMessage(i18n.newSession, { sessionId: newSessionId }) });
}
await fetchSessions(scheduleId);
await fetchSchedule(scheduleId);
@@ -116,7 +174,7 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
const errorMsg = errorMessage(err, 'Failed to trigger schedule');
trackScheduleRunNow(false, getErrorType(err));
toastError({
- title: 'Run Schedule Error',
+ title: intl.formatMessage(i18n.runScheduleError),
msg: errorMsg,
});
} finally {
@@ -130,16 +188,16 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
try {
if (scheduleDetails.paused) {
await unpauseSchedule(scheduleId);
- toastSuccess({ title: 'Schedule Unpaused', msg: `Unpaused "${scheduleId}"` });
+ toastSuccess({ title: intl.formatMessage(i18n.scheduleUnpaused), msg: intl.formatMessage(i18n.unpausedMsg, { id: scheduleId }) });
} else {
await pauseSchedule(scheduleId);
- toastSuccess({ title: 'Schedule Paused', msg: `Paused "${scheduleId}"` });
+ toastSuccess({ title: intl.formatMessage(i18n.schedulePaused), msg: intl.formatMessage(i18n.pausedMsg, { id: scheduleId }) });
}
await fetchSchedule(scheduleId);
} catch (err) {
const errorMsg = errorMessage(err, 'Operation failed');
toastError({
- title: 'Pause/Unpause Error',
+ title: intl.formatMessage(i18n.pauseUnpauseError),
msg: errorMsg,
});
} finally {
@@ -152,12 +210,12 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
setIsActionLoading(true);
try {
const result = await killRunningJob(scheduleId);
- toastSuccess({ title: 'Job Killed', msg: result.message });
+ toastSuccess({ title: intl.formatMessage(i18n.jobKilled), msg: result.message });
await fetchSchedule(scheduleId);
} catch (err) {
const errorMsg = errorMessage(err, 'Failed to kill job');
toastError({
- title: 'Kill Job Error',
+ title: intl.formatMessage(i18n.killJobError),
msg: errorMsg,
});
} finally {
@@ -175,16 +233,16 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
? `${Math.floor(result.runningDurationSeconds / 60)}m ${result.runningDurationSeconds % 60}s`
: 'Unknown';
toastSuccess({
- title: 'Job Inspection',
+ title: intl.formatMessage(i18n.jobInspection),
msg: `Session: ${result.sessionId}\nRunning for: ${duration}`,
});
} else {
- toastSuccess({ title: 'Job Inspection', msg: 'No detailed information available' });
+ toastSuccess({ title: intl.formatMessage(i18n.jobInspection), msg: intl.formatMessage(i18n.inspectNoInfo) });
}
} catch (err) {
const errorMsg = errorMessage(err, 'Failed to inspect job');
toastError({
- title: 'Inspect Job Error',
+ title: intl.formatMessage(i18n.inspectJobError),
msg: errorMsg,
});
} finally {
@@ -197,13 +255,13 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
setIsActionLoading(true);
try {
await updateSchedule(scheduleId, payload as string);
- toastSuccess({ title: 'Schedule Updated', msg: `Updated "${scheduleId}"` });
+ toastSuccess({ title: intl.formatMessage(i18n.scheduleUpdated), msg: intl.formatMessage(i18n.updatedMsg, { id: scheduleId }) });
await fetchSchedule(scheduleId);
setIsModalOpen(false);
} catch (err) {
const errorMsg = errorMessage(err, 'Failed to update schedule');
toastError({
- title: 'Update Schedule Error',
+ title: intl.formatMessage(i18n.updateScheduleError),
msg: errorMsg,
});
} finally {
@@ -223,7 +281,7 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
} catch (err) {
const msg = errorMessage(err, 'Failed to load session');
setSessionError(msg);
- toastError({ title: 'Failed to load session', msg });
+ toastError({ title: intl.formatMessage(i18n.failedToLoadSession), msg });
} finally {
setIsLoadingSession(false);
}
@@ -246,9 +304,9 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
return (
-
Schedule Not Found
+
{intl.formatMessage(i18n.scheduleNotFound)}
- No schedule ID provided. Return to schedules list.
+ {intl.formatMessage(i18n.noScheduleId)}
);
@@ -268,22 +326,22 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
-
Schedule Details
-
Viewing Schedule ID: {scheduleId}
+
{intl.formatMessage(i18n.scheduleDetails)}
+
{intl.formatMessage(i18n.viewingScheduleId, { id: scheduleId })}
- Schedule Information
+ {intl.formatMessage(i18n.scheduleInformation)}
{isLoadingSchedule && (
- Loading schedule...
+ {intl.formatMessage(i18n.loadingSchedule)}
)}
{scheduleError && (
- Error: {scheduleError}
+ {intl.formatMessage(i18n.errorPrefix, { error: scheduleError })}
)}
{scheduleDetails && (
@@ -297,39 +355,39 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
{scheduleDetails.currently_running && (
- Currently Running
+ {intl.formatMessage(i18n.currentlyRunning)}
)}
{scheduleDetails.paused && (
- Paused
+ {intl.formatMessage(i18n.paused)}
)}
- Schedule: {readableCron}
+ {intl.formatMessage(i18n.scheduleLabel)} {readableCron}
- Cron Expression: {scheduleDetails.cron}
+ {intl.formatMessage(i18n.cronExpression)} {scheduleDetails.cron}
- Recipe Source: {scheduleDetails.source}
+ {intl.formatMessage(i18n.recipeSource)} {scheduleDetails.source}
- Last Run: {' '}
+ {intl.formatMessage(i18n.lastRun)} {' '}
{formatToLocalDateWithTimezone(scheduleDetails.last_run)}
{scheduleDetails.currently_running && scheduleDetails.current_session_id && (
- Current Session: {' '}
+ {intl.formatMessage(i18n.currentSession)} {' '}
{scheduleDetails.current_session_id}
)}
{scheduleDetails.currently_running && scheduleDetails.process_start_time && (
- Process Started: {' '}
+ {intl.formatMessage(i18n.processStarted)} {' '}
{formatToLocalDateWithTimezone(scheduleDetails.process_start_time)}
)}
@@ -339,14 +397,14 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
- Actions
+ {intl.formatMessage(i18n.actions)}
- Run Schedule Now
+ {intl.formatMessage(i18n.runScheduleNow)}
{scheduleDetails && !scheduleDetails.currently_running && (
@@ -358,7 +416,7 @@ const ScheduleDetailView: React.FC
= ({ scheduleId, onN
disabled={isActionLoading}
>
- Edit Schedule
+ {intl.formatMessage(i18n.editSchedule)}
= ({ scheduleId, onN
{scheduleDetails.paused ? (
<>
- Unpause Schedule
+ {intl.formatMessage(i18n.unpauseSchedule)}
>
) : (
<>
- Pause Schedule
+ {intl.formatMessage(i18n.pauseSchedule)}
>
)}
@@ -394,7 +452,7 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
disabled={isActionLoading}
>
- Inspect Running Job
+ {intl.formatMessage(i18n.inspectRunningJob)}
= ({ scheduleId, onN
disabled={isActionLoading}
>
- Kill Running Job
+ {intl.formatMessage(i18n.killRunningJob)}
>
)}
@@ -411,29 +469,28 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
{scheduleDetails?.currently_running && (
- Cannot trigger or modify a schedule while it's already running.
+ {intl.formatMessage(i18n.cannotModifyRunning)}
)}
{scheduleDetails?.paused && (
- This schedule is paused and will not run automatically. Use "Run Schedule Now" to
- trigger it manually or unpause to resume automatic execution.
+ {intl.formatMessage(i18n.pausedWarning)}
)}
- Recent Sessions
- {isLoadingSessions && Loading sessions...
}
+ {intl.formatMessage(i18n.recentSessions)}
+ {isLoadingSessions && {intl.formatMessage(i18n.loadingSessions)}
}
{sessionsError && (
- Error: {sessionsError}
+ {intl.formatMessage(i18n.errorPrefix, { error: sessionsError })}
)}
{!isLoadingSessions && sessions.length === 0 && (
- No sessions found for this schedule.
+ {intl.formatMessage(i18n.noSessions)}
)}
@@ -449,15 +506,14 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
className="text-sm font-semibold text-text-primary truncate"
title={session.name || session.id}
>
- {session.name || `Session ID: ${session.id}`}
+ {session.name || intl.formatMessage(i18n.sessionId, { id: session.id })}
- Created:{' '}
- {session.createdAt ? formatToLocalDateWithTimezone(session.createdAt) : 'N/A'}
+ {intl.formatMessage(i18n.created, { date: session.createdAt ? formatToLocalDateWithTimezone(session.createdAt) : 'N/A' })}
{session.messageCount !== undefined && (
- Messages: {session.messageCount}
+ {intl.formatMessage(i18n.messages, { count: session.messageCount })}
)}
{session.workingDir && (
@@ -465,17 +521,17 @@ const ScheduleDetailView: React.FC = ({ scheduleId, onN
className="text-xs text-text-secondary mt-1 truncate"
title={session.workingDir}
>
- Dir: {session.workingDir}
+ {intl.formatMessage(i18n.dir, { path: session.workingDir })}
)}
{session.accumulatedTotalTokens !== undefined &&
session.accumulatedTotalTokens !== null && (
- Tokens: {session.accumulatedTotalTokens}
+ {intl.formatMessage(i18n.tokens, { count: session.accumulatedTotalTokens })}
)}
- ID: {session.id}
+ {intl.formatMessage(i18n.idLabel)} {session.id}
))}
diff --git a/ui/desktop/src/components/schedule/ScheduleModal.tsx b/ui/desktop/src/components/schedule/ScheduleModal.tsx
index 725464a4..5e3d186a 100644
--- a/ui/desktop/src/components/schedule/ScheduleModal.tsx
+++ b/ui/desktop/src/components/schedule/ScheduleModal.tsx
@@ -7,6 +7,35 @@ import { CronPicker } from './CronPicker';
import { Recipe, parseDeeplink, parseRecipeFromFile } from '../../recipe';
import { getStorageDirectory } from '../../recipe/recipe_management';
import ClockIcon from '../../assets/clock-icon.svg';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ editSchedule: { id: 'scheduleModal.editSchedule', defaultMessage: 'Edit Schedule' },
+ createNewSchedule: { id: 'scheduleModal.createNewSchedule', defaultMessage: 'Create New Schedule' },
+ nameLabel: { id: 'scheduleModal.nameLabel', defaultMessage: 'Name:' },
+ namePlaceholder: { id: 'scheduleModal.namePlaceholder', defaultMessage: 'e.g., daily-summary-job' },
+ sourceLabel: { id: 'scheduleModal.sourceLabel', defaultMessage: 'Source:' },
+ yaml: { id: 'scheduleModal.yaml', defaultMessage: 'YAML' },
+ deepLink: { id: 'scheduleModal.deepLink', defaultMessage: 'Deep link' },
+ browseYaml: { id: 'scheduleModal.browseYaml', defaultMessage: 'Browse for YAML file...' },
+ selected: { id: 'scheduleModal.selected', defaultMessage: 'Selected: {path}' },
+ deepLinkPlaceholder: { id: 'scheduleModal.deepLinkPlaceholder', defaultMessage: 'Paste goose://recipe link here...' },
+ recipeParsed: { id: 'scheduleModal.recipeParsed', defaultMessage: 'Recipe parsed successfully' },
+ recipeTitle: { id: 'scheduleModal.recipeTitle', defaultMessage: 'Title: {title}' },
+ recipeDescription: { id: 'scheduleModal.recipeDescription', defaultMessage: 'Description: {description}' },
+ scheduleLabel: { id: 'scheduleModal.scheduleLabel', defaultMessage: 'Schedule:' },
+ cancel: { id: 'scheduleModal.cancel', defaultMessage: 'Cancel' },
+ updating: { id: 'scheduleModal.updating', defaultMessage: 'Updating...' },
+ creating: { id: 'scheduleModal.creating', defaultMessage: 'Creating...' },
+ updateSchedule: { id: 'scheduleModal.updateSchedule', defaultMessage: 'Update Schedule' },
+ createSchedule: { id: 'scheduleModal.createSchedule', defaultMessage: 'Create Schedule' },
+ invalidDeepLink: { id: 'scheduleModal.invalidDeepLink', defaultMessage: 'Invalid deep link. Please use a goose://recipe link.' },
+ failedReadFile: { id: 'scheduleModal.failedReadFile', defaultMessage: 'Failed to read the selected file.' },
+ failedParseRecipe: { id: 'scheduleModal.failedParseRecipe', defaultMessage: 'Failed to parse recipe from file.' },
+ invalidFileType: { id: 'scheduleModal.invalidFileType', defaultMessage: 'Invalid file type: Please select a YAML file (.yaml or .yml)' },
+ scheduleIdRequired: { id: 'scheduleModal.scheduleIdRequired', defaultMessage: 'Schedule ID is required.' },
+ provideValidRecipe: { id: 'scheduleModal.provideValidRecipe', defaultMessage: 'Please provide a valid recipe source.' },
+});
export interface NewSchedulePayload {
id: string;
@@ -37,6 +66,7 @@ export const ScheduleModal: React.FC = ({
apiErrorExternally,
initialDeepLink,
}) => {
+ const intl = useIntl();
const isEditMode = !!schedule;
const [scheduleId, setScheduleId] = useState('');
@@ -70,12 +100,12 @@ export const ScheduleModal: React.FC = ({
}
} catch {
setParsedRecipe(null);
- setInternalValidationError('Invalid deep link. Please use a goose://recipe link.');
+ setInternalValidationError(intl.formatMessage(i18n.invalidDeepLink));
}
} else {
setParsedRecipe(null);
}
- }, []);
+ }, [intl]);
useEffect(() => {
if (isOpen) {
@@ -109,11 +139,11 @@ export const ScheduleModal: React.FC = ({
try {
const fileResponse = await window.electron.readFile(filePath);
if (!fileResponse.found || fileResponse.error) {
- throw new Error('Failed to read the selected file.');
+ throw new Error(intl.formatMessage(i18n.failedReadFile));
}
const recipe = await parseRecipeFromFile(fileResponse.file);
if (!recipe) {
- throw new Error('Failed to parse recipe from file.');
+ throw new Error(intl.formatMessage(i18n.failedParseRecipe));
}
setParsedRecipe(recipe);
if (recipe.title) {
@@ -122,11 +152,11 @@ export const ScheduleModal: React.FC = ({
} catch (e) {
setParsedRecipe(null);
setInternalValidationError(
- e instanceof Error ? e.message : 'Failed to parse recipe from file.'
+ e instanceof Error ? e.message : intl.formatMessage(i18n.failedParseRecipe)
);
}
} else {
- setInternalValidationError('Invalid file type: Please select a YAML file (.yaml or .yml)');
+ setInternalValidationError(intl.formatMessage(i18n.invalidFileType));
}
}
};
@@ -141,12 +171,12 @@ export const ScheduleModal: React.FC = ({
}
if (!scheduleId.trim()) {
- setInternalValidationError('Schedule ID is required.');
+ setInternalValidationError(intl.formatMessage(i18n.scheduleIdRequired));
return;
}
if (!parsedRecipe) {
- setInternalValidationError('Please provide a valid recipe source.');
+ setInternalValidationError(intl.formatMessage(i18n.provideValidRecipe));
return;
}
@@ -169,7 +199,7 @@ export const ScheduleModal: React.FC = ({
- {isEditMode ? 'Edit Schedule' : 'Create New Schedule'}
+ {isEditMode ? intl.formatMessage(i18n.editSchedule) : intl.formatMessage(i18n.createNewSchedule)}
{isEditMode &&
{schedule.id}
}
@@ -196,20 +226,20 @@ export const ScheduleModal: React.FC = ({
<>
- Name:
+ {intl.formatMessage(i18n.nameLabel)}
setScheduleId(e.target.value)}
- placeholder="e.g., daily-summary-job"
+ placeholder={intl.formatMessage(i18n.namePlaceholder)}
required
/>
-
Source:
+
{intl.formatMessage(i18n.sourceLabel)}
= ({
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
- YAML
+ {intl.formatMessage(i18n.yaml)}
= ({
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
- Deep link
+ {intl.formatMessage(i18n.deepLink)}
@@ -244,11 +274,11 @@ export const ScheduleModal: React.FC
= ({
onClick={handleBrowseFile}
className="w-full justify-center rounded-full"
>
- Browse for YAML file...
+ {intl.formatMessage(i18n.browseYaml)}
{recipeSourcePath && (
- Selected: {recipeSourcePath}
+ {intl.formatMessage(i18n.selected, { path: recipeSourcePath })}
)}
@@ -260,19 +290,19 @@ export const ScheduleModal: React.FC
= ({
type="text"
value={deepLinkInput}
onChange={(e) => handleDeepLinkChange(e.target.value)}
- placeholder="Paste goose://recipe link here..."
+ placeholder={intl.formatMessage(i18n.deepLinkPlaceholder)}
className="rounded-full"
/>
{parsedRecipe && (
- ✓ Recipe parsed successfully
+ ✓ {intl.formatMessage(i18n.recipeParsed)}
- Title: {parsedRecipe.title}
+ {intl.formatMessage(i18n.recipeTitle, { title: parsedRecipe.title })}
- Description: {parsedRecipe.description}
+ {intl.formatMessage(i18n.recipeDescription, { description: parsedRecipe.description })}
)}
@@ -284,7 +314,7 @@ export const ScheduleModal: React.FC = ({
)}
- Schedule:
+ {intl.formatMessage(i18n.scheduleLabel)}
@@ -297,7 +327,7 @@ export const ScheduleModal: React.FC = ({
disabled={isLoadingExternally}
className="flex-1 text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800"
>
- Cancel
+ {intl.formatMessage(i18n.cancel)}
= ({
>
{isLoadingExternally
? isEditMode
- ? 'Updating...'
- : 'Creating...'
+ ? intl.formatMessage(i18n.updating)
+ : intl.formatMessage(i18n.creating)
: isEditMode
- ? 'Update Schedule'
- : 'Create Schedule'}
+ ? intl.formatMessage(i18n.updateSchedule)
+ : intl.formatMessage(i18n.createSchedule)}
diff --git a/ui/desktop/src/components/schedule/SchedulesView.tsx b/ui/desktop/src/components/schedule/SchedulesView.tsx
index d5236867..8bc6df9d 100644
--- a/ui/desktop/src/components/schedule/SchedulesView.tsx
+++ b/ui/desktop/src/components/schedule/SchedulesView.tsx
@@ -25,6 +25,39 @@ import { errorMessage } from '../../utils/conversionUtils';
import { MainPanelLayout } from '../Layout/MainPanelLayout';
import { ViewOptions } from '../../utils/navigationUtils';
import { trackScheduleCreated, trackScheduleDeleted, getErrorType } from '../../utils/analytics';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ running: { id: 'schedulesView.running', defaultMessage: 'Running' },
+ paused: { id: 'schedulesView.paused', defaultMessage: 'Paused' },
+ lastRun: { id: 'schedulesView.lastRun', defaultMessage: 'Last run: {date}' },
+ edit: { id: 'schedulesView.edit', defaultMessage: 'Edit' },
+ resume: { id: 'schedulesView.resume', defaultMessage: 'Resume' },
+ pause: { id: 'schedulesView.pause', defaultMessage: 'Pause' },
+ inspect: { id: 'schedulesView.inspect', defaultMessage: 'Inspect' },
+ kill: { id: 'schedulesView.kill', defaultMessage: 'Kill' },
+ scheduler: { id: 'schedulesView.scheduler', defaultMessage: 'Scheduler' },
+ refreshing: { id: 'schedulesView.refreshing', defaultMessage: 'Refreshing...' },
+ refresh: { id: 'schedulesView.refresh', defaultMessage: 'Refresh' },
+ createSchedule: { id: 'schedulesView.createSchedule', defaultMessage: 'Create Schedule' },
+ description: { id: 'schedulesView.description', defaultMessage: 'Create and manage scheduled tasks to run recipes automatically at specified times.' },
+ errorPrefix: { id: 'schedulesView.errorPrefix', defaultMessage: 'Error: {error}' },
+ noSchedules: { id: 'schedulesView.noSchedules', defaultMessage: 'No schedules yet' },
+ scheduleUpdated: { id: 'schedulesView.scheduleUpdated', defaultMessage: 'Schedule Updated' },
+ scheduleUpdatedMsg: { id: 'schedulesView.scheduleUpdatedMsg', defaultMessage: 'Successfully updated schedule "{id}"' },
+ confirmDelete: { id: 'schedulesView.confirmDelete', defaultMessage: 'Are you sure you want to delete schedule "{id}"?' },
+ schedulePaused: { id: 'schedulesView.schedulePaused', defaultMessage: 'Schedule Paused' },
+ schedulePausedMsg: { id: 'schedulesView.schedulePausedMsg', defaultMessage: 'Successfully paused schedule "{id}"' },
+ pauseError: { id: 'schedulesView.pauseError', defaultMessage: 'Pause Schedule Error' },
+ scheduleUnpaused: { id: 'schedulesView.scheduleUnpaused', defaultMessage: 'Schedule Unpaused' },
+ scheduleUnpausedMsg: { id: 'schedulesView.scheduleUnpausedMsg', defaultMessage: 'Successfully unpaused schedule "{id}"' },
+ unpauseError: { id: 'schedulesView.unpauseError', defaultMessage: 'Unpause Schedule Error' },
+ jobKilled: { id: 'schedulesView.jobKilled', defaultMessage: 'Job Killed' },
+ killError: { id: 'schedulesView.killError', defaultMessage: 'Kill Job Error' },
+ jobInspection: { id: 'schedulesView.jobInspection', defaultMessage: 'Job Inspection' },
+ inspectNoInfo: { id: 'schedulesView.inspectNoInfo', defaultMessage: 'No detailed information available for this job' },
+ inspectError: { id: 'schedulesView.inspectError', defaultMessage: 'Inspect Job Error' },
+});
interface SchedulesViewProps {
onClose?: () => void;
@@ -51,6 +84,7 @@ const ScheduleCard: React.FC<{
onDelete,
actionInProgress,
}) => {
+ const intl = useIntl();
let readableCron: string;
try {
readableCron = cronstrue.toString(job.cron);
@@ -74,13 +108,13 @@ const ScheduleCard: React.FC<{
{job.currently_running && (
- Running
+ {intl.formatMessage(i18n.running)}
)}
{job.paused && (
- Paused
+ {intl.formatMessage(i18n.paused)}
)}
@@ -88,7 +122,7 @@ const ScheduleCard: React.FC<{
{readableCron}
- Last run: {formattedLastRun}
+ {intl.formatMessage(i18n.lastRun, { date: formattedLastRun })}
@@ -106,7 +140,7 @@ const ScheduleCard: React.FC<{
className="h-8"
>
- Edit
+ {intl.formatMessage(i18n.edit)}
{
@@ -125,12 +159,12 @@ const ScheduleCard: React.FC<{
{job.paused ? (
<>
- Resume
+ {intl.formatMessage(i18n.resume)}
>
) : (
<>
- Pause
+ {intl.formatMessage(i18n.pause)}
>
)}
@@ -149,7 +183,7 @@ const ScheduleCard: React.FC<{
className="h-8"
>
- Inspect
+ {intl.formatMessage(i18n.inspect)}
{
@@ -162,7 +196,7 @@ const ScheduleCard: React.FC<{
className="h-8"
>
- Kill
+ {intl.formatMessage(i18n.kill)}
>
)}
@@ -185,6 +219,7 @@ const ScheduleCard: React.FC<{
};
const SchedulesView: React.FC
= ({ onClose: _onClose }) => {
+ const intl = useIntl();
const location = useLocation();
const [schedules, setSchedules] = useState([]);
const [isLoading, setIsLoading] = useState(false);
@@ -253,8 +288,8 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
if (editingSchedule) {
await updateSchedule(editingSchedule.id, payload as string);
toastSuccess({
- title: 'Schedule Updated',
- msg: `Successfully updated schedule "${editingSchedule.id}"`,
+ title: intl.formatMessage(i18n.scheduleUpdated),
+ msg: intl.formatMessage(i18n.scheduleUpdatedMsg, { id: editingSchedule.id }),
});
} else {
const newPayload = payload as NewSchedulePayload;
@@ -280,7 +315,7 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
};
const handleDeleteSchedule = async (id: string) => {
- if (!window.confirm(`Are you sure you want to delete schedule "${id}"?`)) return;
+ if (!window.confirm(intl.formatMessage(i18n.confirmDelete, { id }))) return;
setActionsInProgress((prev) => new Set(prev).add(id));
if (viewingScheduleId === id) setViewingScheduleId(null);
@@ -311,8 +346,8 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
try {
await pauseSchedule(id);
toastSuccess({
- title: 'Schedule Paused',
- msg: `Successfully paused schedule "${id}"`,
+ title: intl.formatMessage(i18n.schedulePaused),
+ msg: intl.formatMessage(i18n.schedulePausedMsg, { id }),
});
await fetchSchedules();
} catch (error) {
@@ -320,7 +355,7 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
const errorMsg = errorMessage(error, `Unknown error pausing "${id}".`);
setApiError(errorMsg);
toastError({
- title: 'Pause Schedule Error',
+ title: intl.formatMessage(i18n.pauseError),
msg: errorMsg,
});
} finally {
@@ -339,8 +374,8 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
try {
await unpauseSchedule(id);
toastSuccess({
- title: 'Schedule Unpaused',
- msg: `Successfully unpaused schedule "${id}"`,
+ title: intl.formatMessage(i18n.scheduleUnpaused),
+ msg: intl.formatMessage(i18n.scheduleUnpausedMsg, { id }),
});
await fetchSchedules();
} catch (error) {
@@ -348,7 +383,7 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
const errorMsg = errorMessage(error, `Unknown error unpausing "${id}".`);
setApiError(errorMsg);
toastError({
- title: 'Unpause Schedule Error',
+ title: intl.formatMessage(i18n.unpauseError),
msg: errorMsg,
});
} finally {
@@ -367,7 +402,7 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
try {
const result = await killRunningJob(id);
toastSuccess({
- title: 'Job Killed',
+ title: intl.formatMessage(i18n.jobKilled),
msg: result.message,
});
await fetchSchedules();
@@ -376,7 +411,7 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
const errorMsg = errorMessage(error, `Unknown error killing job "${id}".`);
setApiError(errorMsg);
toastError({
- title: 'Kill Job Error',
+ title: intl.formatMessage(i18n.killError),
msg: errorMsg,
});
} finally {
@@ -399,13 +434,13 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
? `${Math.floor(result.runningDurationSeconds / 60)}m ${result.runningDurationSeconds % 60}s`
: 'Unknown';
toastSuccess({
- title: 'Job Inspection',
+ title: intl.formatMessage(i18n.jobInspection),
msg: `Session: ${result.sessionId}\nRunning for: ${duration}`,
});
} else {
toastSuccess({
- title: 'Job Inspection',
- msg: 'No detailed information available for this job',
+ title: intl.formatMessage(i18n.jobInspection),
+ msg: intl.formatMessage(i18n.inspectNoInfo),
});
}
} catch (error) {
@@ -413,7 +448,7 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
const errorMsg = errorMessage(error, `Unknown error inspecting job "${id}".`);
setApiError(errorMsg);
toastError({
- title: 'Inspect Job Error',
+ title: intl.formatMessage(i18n.inspectError),
msg: errorMsg,
});
} finally {
@@ -445,7 +480,7 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
-
Scheduler
+
{intl.formatMessage(i18n.scheduler)}
= ({ onClose: _onClose }) => {
className="flex items-center gap-2"
>
- {isRefreshing ? 'Refreshing...' : 'Refresh'}
+ {isRefreshing ? intl.formatMessage(i18n.refreshing) : intl.formatMessage(i18n.refresh)}
{
@@ -466,12 +501,12 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
className="flex items-center gap-2"
>
- Create Schedule
+ {intl.formatMessage(i18n.createSchedule)}
- Create and manage scheduled tasks to run recipes automatically at specified times.
+ {intl.formatMessage(i18n.description)}
@@ -481,7 +516,7 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => {
{apiError && (
-
Error: {apiError}
+
{intl.formatMessage(i18n.errorPrefix, { error: apiError })}
)}
@@ -495,7 +530,7 @@ const SchedulesView: React.FC
= ({ onClose: _onClose }) => {
- No schedules yet
+ {intl.formatMessage(i18n.noSchedules)}
)}
diff --git a/ui/desktop/src/components/sessions/SessionHistoryView.tsx b/ui/desktop/src/components/sessions/SessionHistoryView.tsx
index b2c7ad1b..562af8a1 100644
--- a/ui/desktop/src/components/sessions/SessionHistoryView.tsx
+++ b/ui/desktop/src/components/sessions/SessionHistoryView.tsx
@@ -11,6 +11,7 @@ import {
LoaderCircle,
AlertCircle,
} from 'lucide-react';
+import { defineMessages, useIntl } from '../../i18n';
import { resumeSession } from '../../sessions';
import { Button } from '../ui/button';
import { toast } from 'react-toastify';
@@ -34,6 +35,77 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip';
import { Message, Session } from '../../api';
import { useNavigation } from '../../hooks/useNavigation';
+const i18n = defineMessages({
+ errorLoadingDetails: {
+ id: 'sessionHistory.error.loading',
+ defaultMessage: 'Error Loading Session Details',
+ },
+ tryAgain: {
+ id: 'sessionHistory.error.tryAgain',
+ defaultMessage: 'Try Again',
+ },
+ searchPlaceholder: {
+ id: 'sessionHistory.searchPlaceholder',
+ defaultMessage: 'Search history...',
+ },
+ noMessages: {
+ id: 'sessionHistory.empty.title',
+ defaultMessage: 'No messages found',
+ },
+ noMessagesDesc: {
+ id: 'sessionHistory.empty.description',
+ defaultMessage: "This session doesn't contain any messages",
+ },
+ loadingDetails: {
+ id: 'sessionHistory.loading',
+ defaultMessage: 'Loading session details...',
+ },
+ sharing: {
+ id: 'sessionHistory.sharing',
+ defaultMessage: 'Sharing...',
+ },
+ share: {
+ id: 'sessionHistory.share',
+ defaultMessage: 'Share',
+ },
+ shareTooltip: {
+ id: 'sessionHistory.shareTooltip',
+ defaultMessage: 'To enable session sharing, go to Settings > Session > Session Sharing .',
+ },
+ resume: {
+ id: 'sessionHistory.resume',
+ defaultMessage: 'Resume',
+ },
+ shareSessionTitle: {
+ id: 'sessionHistory.shareModal.title',
+ defaultMessage: 'Share Session (beta)',
+ },
+ shareSessionDescription: {
+ id: 'sessionHistory.shareModal.description',
+ defaultMessage: 'Share this session link to give others a read only view of your goose chat.',
+ },
+ copy: {
+ id: 'sessionHistory.copy',
+ defaultMessage: 'Copy',
+ },
+ cancel: {
+ id: 'sessionHistory.cancel',
+ defaultMessage: 'Cancel',
+ },
+ failedToShare: {
+ id: 'sessionHistory.toast.shareFailed',
+ defaultMessage: 'Failed to share session: {error}',
+ },
+ failedToCopy: {
+ id: 'sessionHistory.toast.copyFailed',
+ defaultMessage: 'Failed to copy link to clipboard',
+ },
+ couldNotLaunch: {
+ id: 'sessionHistory.toast.launchFailed',
+ defaultMessage: 'Could not launch session: {error}',
+ },
+});
+
const isUserMessage = (message: Message): boolean => {
if (message.role === 'assistant') {
return false;
@@ -81,6 +153,7 @@ const SessionMessages: React.FC<{
error: string | null;
onRetry: () => void;
}> = ({ messages, isLoading, error, onRetry }) => {
+ const intl = useIntl();
const filteredMessages = filterMessagesForDisplay(messages);
return (
@@ -96,15 +169,15 @@ const SessionMessages: React.FC<{
- Error Loading Session Details
+ {intl.formatMessage(i18n.errorLoadingDetails)}
{error}
- Try Again
+ {intl.formatMessage(i18n.tryAgain)}
) : filteredMessages?.length > 0 ? (
-
+
- No messages found
- This session doesn't contain any messages
+ {intl.formatMessage(i18n.noMessages)}
+ {intl.formatMessage(i18n.noMessagesDesc)}
)}
@@ -146,6 +219,7 @@ const SessionHistoryView: React.FC
= ({
const [isCopied, setIsCopied] = useState(false);
const [canShare, setCanShare] = useState(false);
+ const intl = useIntl();
const messages = session.conversation || [];
const setView = useNavigation();
@@ -180,7 +254,7 @@ const SessionHistoryView: React.FC = ({
setIsShareModalOpen(true);
} catch (error) {
console.error('Error sharing session:', error);
- toast.error(`Failed to share session: ${errorMessage(error, 'Unknown error')}`);
+ toast.error(intl.formatMessage(i18n.failedToShare, { error: errorMessage(error, 'Unknown error') }));
} finally {
setIsSharing(false);
}
@@ -195,7 +269,7 @@ const SessionHistoryView: React.FC = ({
})
.catch((err) => {
console.error('Failed to copy link:', err);
- toast.error('Failed to copy link to clipboard');
+ toast.error(intl.formatMessage(i18n.failedToCopy));
});
};
@@ -203,7 +277,7 @@ const SessionHistoryView: React.FC = ({
try {
resumeSession(session, setView);
} catch (error) {
- toast.error(`Could not launch session: ${errorMessage(error)}`);
+ toast.error(intl.formatMessage(i18n.couldNotLaunch, { error: errorMessage(error) }));
}
};
@@ -221,12 +295,12 @@ const SessionHistoryView: React.FC = ({
{isSharing ? (
<>
- Sharing...
+ {intl.formatMessage(i18n.sharing)}
>
) : (
<>
- Share
+ {intl.formatMessage(i18n.share)}
>
)}
@@ -234,15 +308,16 @@ const SessionHistoryView: React.FC = ({
{!canShare ? (
- To enable session sharing, go to Settings {'>'} Session {'>'}{' '}
- Session Sharing .
+ {intl.formatMessage(i18n.shareTooltip, {
+ b: (chunks: React.ReactNode) => {chunks} ,
+ })}
) : null}
- Resume
+ {intl.formatMessage(i18n.resume)}
>
) : null;
@@ -285,7 +360,7 @@ const SessionHistoryView: React.FC = ({
) : (
- Loading session details...
+ {intl.formatMessage(i18n.loadingDetails)}
)}
@@ -305,10 +380,10 @@ const SessionHistoryView: React.FC
= ({
- Share Session (beta)
+ {intl.formatMessage(i18n.shareSessionTitle)}
- Share this session link to give others a read only view of your goose chat.
+ {intl.formatMessage(i18n.shareSessionDescription)}
@@ -325,14 +400,14 @@ const SessionHistoryView: React.FC = ({
disabled={isCopied}
>
{isCopied ? : }
- Copy
+ {intl.formatMessage(i18n.copy)}
setIsShareModalOpen(false)}>
- Cancel
+ {intl.formatMessage(i18n.cancel)}
diff --git a/ui/desktop/src/components/sessions/SessionItem.tsx b/ui/desktop/src/components/sessions/SessionItem.tsx
index 4b234a6d..3c807031 100644
--- a/ui/desktop/src/components/sessions/SessionItem.tsx
+++ b/ui/desktop/src/components/sessions/SessionItem.tsx
@@ -1,16 +1,25 @@
import React from 'react';
+import { defineMessages, useIntl } from '../../i18n';
import { Card } from '../ui/card';
import { formatDate } from '../../utils/date';
import { Session } from '../../api';
import { shouldShowNewChatTitle } from '../../sessions';
import { DEFAULT_CHAT_TITLE } from '../../contexts/ChatContext';
+const i18n = defineMessages({
+ messageCount: {
+ id: 'sessionItem.messageCount',
+ defaultMessage: '{count} messages',
+ },
+});
+
interface SessionItemProps {
session: Session;
extraActions?: React.ReactNode;
}
const SessionItem: React.FC
= ({ session, extraActions }) => {
+ const intl = useIntl();
const displayName = shouldShowNewChatTitle(session) ? DEFAULT_CHAT_TITLE : session.name;
return (
@@ -18,7 +27,7 @@ const SessionItem: React.FC = ({ session, extraActions }) => {
{displayName}
- {formatDate(session.updated_at)} • {session.message_count} messages
+ {formatDate(session.updated_at)} • {intl.formatMessage(i18n.messageCount, { count: session.message_count })}
{session.working_dir}
diff --git a/ui/desktop/src/components/sessions/SessionListView.tsx b/ui/desktop/src/components/sessions/SessionListView.tsx
index f9d0d931..3f86e12c 100644
--- a/ui/desktop/src/components/sessions/SessionListView.tsx
+++ b/ui/desktop/src/components/sessions/SessionListView.tsx
@@ -1,5 +1,6 @@
import { AppEvents } from '../../constants/events';
import React, { useEffect, useState, useRef, useCallback, useMemo, startTransition } from 'react';
+import { defineMessages, useIntl } from '../../i18n';
import {
MessageSquareText,
Target,
@@ -44,6 +45,42 @@ import { getSearchShortcutText } from '../../utils/keyboardShortcuts';
import { shouldShowNewChatTitle } from '../../sessions';
import { DEFAULT_CHAT_TITLE } from '../../contexts/ChatContext';
+const i18n = defineMessages({
+ editSessionTitle: { id: 'sessions.edit.title', defaultMessage: 'Edit Session Description' },
+ editSessionPlaceholder: { id: 'sessions.edit.placeholder', defaultMessage: 'Enter session description' },
+ cancel: { id: 'sessions.cancel', defaultMessage: 'Cancel' },
+ save: { id: 'sessions.save', defaultMessage: 'Save' },
+ saving: { id: 'sessions.saving', defaultMessage: 'Saving...' },
+ sessionUpdated: { id: 'sessions.toast.updated', defaultMessage: 'Session description updated successfully' },
+ sessionUpdateFailed: { id: 'sessions.toast.updateFailed', defaultMessage: 'Failed to update session description: {error}' },
+ chatHistory: { id: 'sessions.chatHistory', defaultMessage: 'Chat history' },
+ importSession: { id: 'sessions.import', defaultMessage: 'Import Session' },
+ chatHistoryDesc: { id: 'sessions.chatHistoryDesc', defaultMessage: 'View and search your past conversations with Goose. {shortcut} to search.' },
+ searchPlaceholder: { id: 'sessions.searchPlaceholder', defaultMessage: 'Search history...' },
+ errorLoading: { id: 'sessions.error.loading', defaultMessage: 'Error Loading Sessions' },
+ tryAgain: { id: 'sessions.error.tryAgain', defaultMessage: 'Try Again' },
+ noSessions: { id: 'sessions.empty.title', defaultMessage: 'No chat sessions found' },
+ noSessionsDesc: { id: 'sessions.empty.description', defaultMessage: 'Your chat history will appear here' },
+ noMatching: { id: 'sessions.search.noResults', defaultMessage: 'No matching sessions found' },
+ noMatchingDesc: { id: 'sessions.search.noResultsDesc', defaultMessage: 'Try adjusting your search terms' },
+ loadingMore: { id: 'sessions.loadingMore', defaultMessage: 'Loading more sessions...' },
+ deleteTitle: { id: 'sessions.delete.title', defaultMessage: 'Delete Session' },
+ deleteMessage: { id: 'sessions.delete.message', defaultMessage: 'Are you sure you want to delete the session "{name}"? This action cannot be undone.' },
+ duplicateSuccess: { id: 'sessions.toast.duplicated', defaultMessage: 'Session "{name}" duplicated successfully' },
+ duplicateFailed: { id: 'sessions.toast.duplicateFailed', defaultMessage: 'Failed to duplicate session: {error}' },
+ deleteSuccess: { id: 'sessions.toast.deleted', defaultMessage: 'Session deleted successfully' },
+ deleteFailed: { id: 'sessions.toast.deleteFailed', defaultMessage: 'Failed to delete session "{name}": {error}' },
+ importSuccess: { id: 'sessions.toast.imported', defaultMessage: 'Session imported successfully' },
+ importFailed: { id: 'sessions.toast.importFailed', defaultMessage: 'Failed to import session: {error}' },
+ exportSuccess: { id: 'sessions.toast.exported', defaultMessage: 'Session exported successfully' },
+ openInNewWindow: { id: 'sessions.action.openNewWindow', defaultMessage: 'Open in new window' },
+ editSessionName: { id: 'sessions.action.editName', defaultMessage: 'Edit session name' },
+ duplicateSession: { id: 'sessions.action.duplicate', defaultMessage: 'Duplicate session' },
+ deleteSession: { id: 'sessions.action.delete', defaultMessage: 'Delete session' },
+ exportSession: { id: 'sessions.action.export', defaultMessage: 'Export session' },
+ extensions: { id: 'sessions.extensions', defaultMessage: 'Extensions:' },
+});
+
function getSessionExtensionNames(extensionData: ExtensionData): string[] {
try {
const enabledExtensionData = extensionData?.['enabled_extensions.v0'] as
@@ -67,6 +104,7 @@ interface EditSessionModalProps {
const EditSessionModal = React.memo(
({ session, isOpen, onClose, onSave, disabled = false }) => {
+ const intl = useIntl();
const [description, setDescription] = useState('');
const [isUpdating, setIsUpdating] = useState(false);
@@ -98,17 +136,17 @@ const EditSessionModal = React.memo(
await onSave(session.id, trimmedDescription);
onClose();
setTimeout(() => {
- toast.success('Session description updated successfully');
+ toast.success(intl.formatMessage(i18n.sessionUpdated));
}, 300);
} catch (error) {
const errMsg = errorMessage(error, 'Unknown error occurred');
console.error('Failed to update session description:', errMsg);
- toast.error(`Failed to update session description: ${errMsg}`);
+ toast.error(intl.formatMessage(i18n.sessionUpdateFailed, { error: errMsg }));
setDescription(session.name);
} finally {
setIsUpdating(false);
}
- }, [session, description, onSave, onClose, disabled]);
+ }, [session, description, onSave, onClose, disabled, intl]);
const handleCancel = useCallback(() => {
if (!isUpdating) {
@@ -136,7 +174,7 @@ const EditSessionModal = React.memo(
return (
-
Edit Session Description
+
{intl.formatMessage(i18n.editSessionTitle)}
@@ -146,7 +184,7 @@ const EditSessionModal = React.memo
(
value={description}
onChange={handleInputChange}
className="w-full p-3 border border-border-primary rounded-lg bg-background-primary text-text-primary focus:outline-none focus:ring-2 focus:ring-blue-500"
- placeholder="Enter session description"
+ placeholder={intl.formatMessage(i18n.editSessionPlaceholder)}
autoFocus
maxLength={200}
onKeyDown={handleKeyDown}
@@ -157,14 +195,14 @@ const EditSessionModal = React.memo(
- Cancel
+ {intl.formatMessage(i18n.cancel)}
- {isUpdating ? 'Saving...' : 'Save'}
+ {isUpdating ? intl.formatMessage(i18n.saving) : intl.formatMessage(i18n.save)}
@@ -203,6 +241,7 @@ interface SessionListViewProps {
const SessionListView: React.FC
= React.memo(
({ onSelectSession, selectedSessionId }) => {
+ const intl = useIntl();
const [sessions, setSessions] = useState([]);
const [filteredSessions, setFilteredSessions] = useState([]);
const [dateGroups, setDateGroups] = useState([]);
@@ -443,14 +482,14 @@ const SessionListView: React.FC = React.memo(
body: { truncate: false, copy: true },
throwOnError: true,
});
- toast.success(`Session "${session.name}" duplicated successfully`);
+ toast.success(intl.formatMessage(i18n.duplicateSuccess, { name: session.name }));
await loadSessions();
} catch (error) {
console.error('Error duplicating session:', error);
- toast.error(`Failed to duplicate session: ${errorMessage(error, 'Unknown error')}`);
+ toast.error(intl.formatMessage(i18n.duplicateFailed, { error: errorMessage(error, 'Unknown error') }));
}
},
- [loadSessions]
+ [loadSessions, intl]
);
const handleConfirmDelete = useCallback(async () => {
@@ -466,18 +505,16 @@ const SessionListView: React.FC = React.memo(
path: { session_id: sessionToDeleteId },
throwOnError: true,
});
- toast.success('Session deleted successfully');
+ toast.success(intl.formatMessage(i18n.deleteSuccess));
window.dispatchEvent(
new CustomEvent(AppEvents.SESSION_DELETED, { detail: { sessionId: sessionToDeleteId } })
);
} catch (error) {
console.error('Error deleting session:', error);
- toast.error(
- `Failed to delete session "${sessionName}": ${errorMessage(error, 'Unknown error')}`
- );
+ toast.error(intl.formatMessage(i18n.deleteFailed, { name: sessionName, error: errorMessage(error, 'Unknown error') }));
}
await loadSessions();
- }, [sessionToDelete, loadSessions]);
+ }, [sessionToDelete, loadSessions, intl]);
const handleCancelDelete = useCallback(() => {
setShowDeleteConfirmation(false);
@@ -502,8 +539,8 @@ const SessionListView: React.FC = React.memo(
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
- toast.success('Session exported successfully');
- }, []);
+ toast.success(intl.formatMessage(i18n.exportSuccess));
+ }, [intl]);
const handleImportClick = useCallback(() => {
fileInputRef.current?.click();
@@ -521,17 +558,17 @@ const SessionListView: React.FC = React.memo(
throwOnError: true,
});
- toast.success('Session imported successfully');
+ toast.success(intl.formatMessage(i18n.importSuccess));
await loadSessions();
} catch (error) {
- toast.error(`Failed to import session: ${error}`);
+ toast.error(intl.formatMessage(i18n.importFailed, { error: String(error) }));
} finally {
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
},
- [loadSessions]
+ [loadSessions, intl]
);
const handleOpenInNewWindow = useCallback((session: Session, e: React.MouseEvent) => {
@@ -650,7 +687,7 @@ const SessionListView: React.FC = React.memo(
-
Extensions:
+
{intl.formatMessage(i18n.extensions)}
{extensionNames.map((name) => (
{name}
@@ -667,35 +704,35 @@ const SessionListView: React.FC = React.memo(
@@ -746,10 +783,10 @@ const SessionListView: React.FC = React.memo(
return (
-
Error Loading Sessions
+
{intl.formatMessage(i18n.errorLoading)}
{error}
- Try Again
+ {intl.formatMessage(i18n.tryAgain)}
);
@@ -759,8 +796,8 @@ const SessionListView: React.FC = React.memo(
return (
-
No chat sessions found
-
Your chat history will appear here
+
{intl.formatMessage(i18n.noSessions)}
+
{intl.formatMessage(i18n.noSessionsDesc)}
);
}
@@ -769,8 +806,8 @@ const SessionListView: React.FC = React.memo(
return (
-
No matching sessions found
-
Try adjusting your search terms
+
{intl.formatMessage(i18n.noMatching)}
+
{intl.formatMessage(i18n.noMatchingDesc)}
);
}
@@ -802,7 +839,7 @@ const SessionListView: React.FC = React.memo(
-
Loading more sessions...
+
{intl.formatMessage(i18n.loadingMore)}
)}
@@ -817,7 +854,7 @@ const SessionListView: React.FC = React.memo(
-
Chat history
+ {intl.formatMessage(i18n.chatHistory)}
= React.memo(
className="flex items-center gap-2"
>
- Import Session
+ {intl.formatMessage(i18n.importSession)}
- View and search your past conversations with Goose. {getSearchShortcutText()} to
- search.
+ {intl.formatMessage(i18n.chatHistoryDesc, { shortcut: getSearchShortcutText() })}
@@ -843,7 +879,7 @@ const SessionListView: React.FC = React.memo(
onNavigate={handleSearchNavigation}
searchResults={searchResults}
className="relative"
- placeholder="Search history..."
+ placeholder={intl.formatMessage(i18n.searchPlaceholder)}
>
{/* Skeleton layer - always rendered but conditionally visible */}
= React.memo(
= ({
error,
onRetry,
}) => {
+ const intl = useIntl();
return (
@@ -73,10 +102,10 @@ export const SessionMessages: React.FC
= ({
- Error Loading Session Details
+ {intl.formatMessage(i18n.errorLoadingDetails)}
{error}
- Try Again
+ {intl.formatMessage(i18n.tryAgain)}
) : messages?.length > 0 ? (
@@ -113,7 +142,7 @@ export const SessionMessages: React.FC = ({
>
- {message.role === 'user' ? 'You' : 'Goose'}
+ {message.role === 'user' ? intl.formatMessage(i18n.you) : intl.formatMessage(i18n.goose)}
{formatMessageTimestamp(message.created)}
@@ -171,8 +200,8 @@ export const SessionMessages: React.FC = ({
) : (
-
No messages found
-
This session doesn't contain any messages
+
{intl.formatMessage(i18n.noMessages)}
+
{intl.formatMessage(i18n.noMessagesDesc)}
)}
diff --git a/ui/desktop/src/components/sessions/SessionsInsights.tsx b/ui/desktop/src/components/sessions/SessionsInsights.tsx
index 229092e9..7d92267e 100644
--- a/ui/desktop/src/components/sessions/SessionsInsights.tsx
+++ b/ui/desktop/src/components/sessions/SessionsInsights.tsx
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
+import { defineMessages, useIntl } from '../../i18n';
import { errorMessage } from '../../utils/conversionUtils';
import { Card, CardContent, CardDescription } from '../ui/card';
import { Greeting } from '../common/Greeting';
@@ -16,7 +17,35 @@ import {
import { resumeSession } from '../../sessions';
import { useNavigation } from '../../hooks/useNavigation';
+const i18n = defineMessages({
+ totalSessions: {
+ id: 'sessionsInsights.totalSessions',
+ defaultMessage: 'Total sessions',
+ },
+ totalTokens: {
+ id: 'sessionsInsights.totalTokens',
+ defaultMessage: 'Total tokens',
+ },
+ recentChats: {
+ id: 'sessionsInsights.recentChats',
+ defaultMessage: 'Recent chats',
+ },
+ seeAll: {
+ id: 'sessionsInsights.seeAll',
+ defaultMessage: 'See all',
+ },
+ noRecentChats: {
+ id: 'sessionsInsights.noRecentChats',
+ defaultMessage: 'No recent chat sessions found.',
+ },
+ failedToLoadInsights: {
+ id: 'sessionsInsights.failedToLoad',
+ defaultMessage: 'Failed to load insights',
+ },
+});
+
export function SessionInsights() {
+ const intl = useIntl();
const [insights, setInsights] = useState(null);
const [error, setError] = useState(null);
const [recentSessions, setRecentSessions] = useState([]);
@@ -138,7 +167,7 @@ export function SessionInsights() {
- Total sessions
+ {intl.formatMessage(i18n.totalSessions)}
@@ -148,7 +177,7 @@ export function SessionInsights() {
- Total tokens
+ {intl.formatMessage(i18n.totalTokens)}
@@ -160,7 +189,7 @@ export function SessionInsights() {
- Recent chats
+ {intl.formatMessage(i18n.recentChats)}
- See all
+ {intl.formatMessage(i18n.seeAll)}
@@ -230,7 +259,7 @@ export function SessionInsights() {
- Failed to load insights
+ {intl.formatMessage(i18n.failedToLoadInsights)}
@@ -245,7 +274,7 @@ export function SessionInsights() {
{Math.max(insights?.totalSessions ?? 0, 0)}
- Total sessions
+ {intl.formatMessage(i18n.totalSessions)}
@@ -271,7 +300,7 @@ export function SessionInsights() {
{formatTokens(insights?.totalTokens)}
- Total tokens
+ {intl.formatMessage(i18n.totalTokens)}
@@ -284,7 +313,7 @@ export function SessionInsights() {
- Recent chats
+ {intl.formatMessage(i18n.recentChats)}
- See all
+ {intl.formatMessage(i18n.seeAll)}
@@ -346,7 +375,7 @@ export function SessionInsights() {
))
) : (
- No recent chat sessions found.
+ {intl.formatMessage(i18n.noRecentChats)}
)}
diff --git a/ui/desktop/src/components/sessions/SessionsView.tsx b/ui/desktop/src/components/sessions/SessionsView.tsx
index 23d26290..a3e8c19a 100644
--- a/ui/desktop/src/components/sessions/SessionsView.tsx
+++ b/ui/desktop/src/components/sessions/SessionsView.tsx
@@ -1,11 +1,24 @@
import React, { useState, useEffect, useCallback } from 'react';
+import { defineMessages, useIntl } from '../../i18n';
import SessionListView from './SessionListView';
import SessionHistoryView from './SessionHistoryView';
import { useLocation } from 'react-router-dom';
import { getSession, Session } from '../../api';
import { useNavigation } from '../../hooks/useNavigation';
+const i18n = defineMessages({
+ loading: {
+ id: 'sessionsView.loading',
+ defaultMessage: 'Loading...',
+ },
+ failedToLoad: {
+ id: 'sessionsView.error.failedToLoad',
+ defaultMessage: 'Failed to load session details. Please try again later.',
+ },
+});
+
const SessionsView: React.FC = () => {
+ const intl = useIntl();
const [selectedSession, setSelectedSession] = useState(null);
const [showSessionHistory, setShowSessionHistory] = useState(false);
const [isLoadingSession, setIsLoadingSession] = useState(false);
@@ -26,7 +39,7 @@ const SessionsView: React.FC = () => {
setSelectedSession(response.data);
} catch (err) {
console.error(`Failed to load session details for ${sessionId}:`, err);
- setError('Failed to load session details. Please try again later.');
+ setError(intl.formatMessage(i18n.failedToLoad));
// Keep the selected session null if there's an error
setSelectedSession(null);
setShowSessionHistory(false);
@@ -78,7 +91,7 @@ const SessionsView: React.FC = () => {
selectedSession || {
id: initialSessionId || '',
conversation: [],
- name: 'Loading...',
+ name: intl.formatMessage(i18n.loading),
working_dir: '',
message_count: 0,
total_tokens: 0,
diff --git a/ui/desktop/src/components/sessions/SharedSessionView.tsx b/ui/desktop/src/components/sessions/SharedSessionView.tsx
index 85d39f41..84c7e7a6 100644
--- a/ui/desktop/src/components/sessions/SharedSessionView.tsx
+++ b/ui/desktop/src/components/sessions/SharedSessionView.tsx
@@ -1,10 +1,22 @@
import React from 'react';
import { Calendar, MessageSquareText, Folder, Target, LoaderCircle, Share2 } from 'lucide-react';
+import { defineMessages, useIntl } from '../../i18n';
import { type SharedSessionDetails } from '../../sharedSessions';
import { SessionMessages } from './SessionViewComponents';
import { formatMessageTimestamp } from '../../utils/timeUtils';
import { MainPanelLayout } from '../Layout/MainPanelLayout';
+const i18n = defineMessages({
+ sharedSession: {
+ id: 'sharedSession.title',
+ defaultMessage: 'Shared Session',
+ },
+ loadingDetails: {
+ id: 'sharedSession.loading',
+ defaultMessage: 'Loading session details...',
+ },
+});
+
interface SharedSessionViewProps {
session: SharedSessionDetails | null;
isLoading: boolean;
@@ -31,17 +43,18 @@ const SharedSessionView: React.FC = ({
error,
onRetry,
}) => {
+ const intl = useIntl();
return (
- Shared Session
+ {intl.formatMessage(i18n.sharedSession)}
-
+
{!isLoading && session && session.messages.length > 0 ? (
<>
@@ -71,7 +84,7 @@ const SharedSessionView: React.FC
= ({
) : (
- Loading session details...
+ {intl.formatMessage(i18n.loadingDetails)}
)}
diff --git a/ui/desktop/src/components/settings/PromptsSettingsSection.tsx b/ui/desktop/src/components/settings/PromptsSettingsSection.tsx
index a0124730..610fb403 100644
--- a/ui/desktop/src/components/settings/PromptsSettingsSection.tsx
+++ b/ui/desktop/src/components/settings/PromptsSettingsSection.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
import {
getPrompt,
getPrompts,
@@ -11,15 +11,124 @@ import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Button } from '../ui/button';
import { AlertTriangle, RotateCcw, ArrowLeft } from 'lucide-react';
import { toast } from 'react-toastify';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ failedToLoadPrompts: {
+ id: 'promptsSettings.failedToLoadPrompts',
+ defaultMessage: 'Failed to load prompts',
+ },
+ failedToLoadPrompt: {
+ id: 'promptsSettings.failedToLoadPrompt',
+ defaultMessage: 'Failed to load prompt',
+ },
+ confirmResetAll: {
+ id: 'promptsSettings.confirmResetAll',
+ defaultMessage: 'Are you sure you want to reset all prompts to their defaults? This cannot be undone.',
+ },
+ allPromptsReset: {
+ id: 'promptsSettings.allPromptsReset',
+ defaultMessage: 'All prompts reset to defaults',
+ },
+ failedToResetPrompts: {
+ id: 'promptsSettings.failedToResetPrompts',
+ defaultMessage: 'Failed to reset prompts',
+ },
+ promptSaved: {
+ id: 'promptsSettings.promptSaved',
+ defaultMessage: 'Prompt saved',
+ },
+ failedToSavePrompt: {
+ id: 'promptsSettings.failedToSavePrompt',
+ defaultMessage: 'Failed to save prompt',
+ },
+ confirmResetOne: {
+ id: 'promptsSettings.confirmResetOne',
+ defaultMessage: 'Are you sure you want to reset this prompt to its default? This cannot be undone.',
+ },
+ promptResetToDefault: {
+ id: 'promptsSettings.promptResetToDefault',
+ defaultMessage: 'Prompt reset to default',
+ },
+ failedToResetPrompt: {
+ id: 'promptsSettings.failedToResetPrompt',
+ defaultMessage: 'Failed to reset prompt',
+ },
+ confirmReplaceWithDefault: {
+ id: 'promptsSettings.confirmReplaceWithDefault',
+ defaultMessage: 'Replace current content with default? Your changes will be lost.',
+ },
+ confirmUnsavedBack: {
+ id: 'promptsSettings.confirmUnsavedBack',
+ defaultMessage: 'You have unsaved changes. Are you sure you want to go back?',
+ },
+ backToList: {
+ id: 'promptsSettings.backToList',
+ defaultMessage: 'Back to List',
+ },
+ resetToDefault: {
+ id: 'promptsSettings.resetToDefault',
+ defaultMessage: 'Reset to Default',
+ },
+ save: {
+ id: 'promptsSettings.save',
+ defaultMessage: 'Save',
+ },
+ editPromptTitle: {
+ id: 'promptsSettings.editPromptTitle',
+ defaultMessage: 'Edit: {name}',
+ },
+ customized: {
+ id: 'promptsSettings.customized',
+ defaultMessage: 'Customized',
+ },
+ templateTip: {
+ id: 'promptsSettings.templateTip',
+ defaultMessage: 'Template variables like {extensionsExample} or {forExample} are replaced with actual values at runtime. Be careful not to remove required variables.',
+ },
+ editingLabel: {
+ id: 'promptsSettings.editingLabel',
+ defaultMessage: 'Editing: {name}',
+ },
+ restoreDefault: {
+ id: 'promptsSettings.restoreDefault',
+ defaultMessage: 'Restore Default',
+ },
+ enterPromptContent: {
+ id: 'promptsSettings.enterPromptContent',
+ defaultMessage: 'Enter prompt content...',
+ },
+ unsavedChanges: {
+ id: 'promptsSettings.unsavedChanges',
+ defaultMessage: 'You have unsaved changes',
+ },
+ promptEditingTitle: {
+ id: 'promptsSettings.promptEditingTitle',
+ defaultMessage: 'Prompt Editing',
+ },
+ promptEditingDescription: {
+ id: 'promptsSettings.promptEditingDescription',
+ defaultMessage: "Customize the prompts that define goose's behavior in different contexts. These prompts use Jinja2 templating syntax. Be careful when modifying template variables, as incorrect changes can break functionality. Please share any improvements with the community.",
+ },
+ resetAll: {
+ id: 'promptsSettings.resetAll',
+ defaultMessage: 'Reset All',
+ },
+ edit: {
+ id: 'promptsSettings.edit',
+ defaultMessage: 'Edit',
+ },
+});
export default function PromptsSettingsSection() {
+ const intl = useIntl();
const [prompts, setPrompts] = useState([]);
const [selectedPrompt, setSelectedPrompt] = useState(null);
const [promptData, setPromptData] = useState(null);
const [content, setContent] = useState('');
const [hasChanges, setHasChanges] = useState(false);
- const fetchPrompts = async () => {
+ const fetchPrompts = useCallback(async () => {
try {
const response = await getPrompts();
if (response.data) {
@@ -27,13 +136,13 @@ export default function PromptsSettingsSection() {
}
} catch (error) {
console.error('Failed to fetch prompts:', error);
- toast.error('Failed to load prompts');
+ toast.error(intl.formatMessage(i18n.failedToLoadPrompts));
}
- };
+ }, [intl]);
useEffect(() => {
fetchPrompts();
- }, []);
+ }, [fetchPrompts]);
useEffect(() => {
if (selectedPrompt) {
@@ -46,12 +155,12 @@ export default function PromptsSettingsSection() {
}
} catch (error) {
console.error('Failed to fetch prompt:', error);
- toast.error('Failed to load prompt');
+ toast.error(intl.formatMessage(i18n.failedToLoadPrompt));
}
};
fetchPrompt();
}
- }, [selectedPrompt]);
+ }, [selectedPrompt, intl]);
useEffect(() => {
if (promptData) {
@@ -62,7 +171,7 @@ export default function PromptsSettingsSection() {
const handleResetAll = async () => {
if (
!window.confirm(
- 'Are you sure you want to reset all prompts to their defaults? This cannot be undone.'
+ intl.formatMessage(i18n.confirmResetAll)
)
) {
return;
@@ -73,11 +182,11 @@ export default function PromptsSettingsSection() {
for (const prompt of customizedPrompts) {
await resetPrompt({ path: { name: prompt.name } });
}
- toast.success('All prompts reset to defaults');
+ toast.success(intl.formatMessage(i18n.allPromptsReset));
fetchPrompts();
} catch (error) {
console.error('Failed to reset all prompts:', error);
- toast.error('Failed to reset prompts');
+ toast.error(intl.formatMessage(i18n.failedToResetPrompts));
}
};
@@ -88,12 +197,12 @@ export default function PromptsSettingsSection() {
path: { name: selectedPrompt },
body: { content },
});
- toast.success('Prompt saved');
+ toast.success(intl.formatMessage(i18n.promptSaved));
setPromptData((prev) => (prev ? { ...prev, content, is_customized: true } : null));
fetchPrompts();
} catch (error) {
console.error('Failed to save prompt:', error);
- toast.error('Failed to save prompt');
+ toast.error(intl.formatMessage(i18n.failedToSavePrompt));
}
};
@@ -101,7 +210,7 @@ export default function PromptsSettingsSection() {
if (!selectedPrompt) return;
if (
!window.confirm(
- 'Are you sure you want to reset this prompt to its default? This cannot be undone.'
+ intl.formatMessage(i18n.confirmResetOne)
)
) {
return;
@@ -114,17 +223,17 @@ export default function PromptsSettingsSection() {
setPromptData({ ...promptData, content: promptData.default_content, is_customized: false });
}
fetchPrompts();
- toast.success('Prompt reset to default');
+ toast.success(intl.formatMessage(i18n.promptResetToDefault));
} catch (error) {
console.error('Failed to reset prompt:', error);
- toast.error('Failed to reset prompt');
+ toast.error(intl.formatMessage(i18n.failedToResetPrompt));
}
};
const handleRestoreDefault = () => {
if (promptData) {
if (hasChanges) {
- if (!window.confirm('Replace current content with default? Your changes will be lost.')) {
+ if (!window.confirm(intl.formatMessage(i18n.confirmReplaceWithDefault))) {
return;
}
}
@@ -134,7 +243,7 @@ export default function PromptsSettingsSection() {
const handleBack = () => {
if (hasChanges) {
- if (!window.confirm('You have unsaved changes. Are you sure you want to go back?')) {
+ if (!window.confirm(intl.formatMessage(i18n.confirmUnsavedBack))) {
return;
}
}
@@ -158,7 +267,7 @@ export default function PromptsSettingsSection() {
className="flex items-center gap-2"
>
- Back to List
+ {intl.formatMessage(i18n.backToList)}
{promptData?.is_customized && (
@@ -169,19 +278,19 @@ export default function PromptsSettingsSection() {
className="flex items-center gap-2"
>
- Reset to Default
+ {intl.formatMessage(i18n.resetToDefault)}
)}
- Save
+ {intl.formatMessage(i18n.save)}
- Edit: {selectedPrompt}
+ {intl.formatMessage(i18n.editPromptTitle, { name: selectedPrompt })}
{promptData?.is_customized && (
- Customized
+ {intl.formatMessage(i18n.customized)}
)}
@@ -189,19 +298,16 @@ export default function PromptsSettingsSection() {
- Tip: Template variables like{' '}
- {'{{ extensions }}'} or{' '}
-
- {'{% for item in list %}'}
- {' '}
- are replaced with actual values at runtime. Be careful not to remove required
- variables.
+ {intl.formatMessage(i18n.templateTip, {
+ extensionsExample: '{{ extensions }}',
+ forExample: '{% for item in list %}',
+ })}
- Editing: {selectedPrompt}
+ {intl.formatMessage(i18n.editingLabel, { name: selectedPrompt })}
{promptData?.is_customized && content !== promptData.default_content && (
- Restore Default
+ {intl.formatMessage(i18n.restoreDefault)}
)}
@@ -217,14 +323,14 @@ export default function PromptsSettingsSection() {
value={content}
className="w-full flex-1 min-h-[500px] border rounded-md p-3 text-sm font-mono resize-y bg-background-primary text-text-primary border-border-primary focus:outline-none focus:ring-2 focus:ring-blue-500"
onChange={(e) => setContent(e.target.value)}
- placeholder="Enter prompt content..."
+ placeholder={intl.formatMessage(i18n.enterPromptContent)}
spellCheck={false}
/>
{hasChanges && (
- You have unsaved changes
+ {intl.formatMessage(i18n.unsavedChanges)}
)}
@@ -240,12 +346,9 @@ export default function PromptsSettingsSection() {
-
Prompt Editing
+
{intl.formatMessage(i18n.promptEditingTitle)}
- Customize the prompts that define goose's behavior in different contexts. These
- prompts use Jinja2 templating syntax. Be careful when modifying template variables,
- as incorrect changes can break functionality. Please share any improvements with the
- community.
+ {intl.formatMessage(i18n.promptEditingDescription)}
{hasCustomizedPrompts && (
@@ -256,7 +359,7 @@ export default function PromptsSettingsSection() {
className="flex items-center gap-2 border-yellow-500/50 hover:bg-yellow-500/20"
>
- Reset All
+ {intl.formatMessage(i18n.resetAll)}
)}
@@ -273,7 +376,7 @@ export default function PromptsSettingsSection() {
{prompt.name}
{prompt.is_customized && (
- Customized
+ {intl.formatMessage(i18n.customized)}
)}
@@ -287,7 +390,7 @@ export default function PromptsSettingsSection() {
onClick={() => setSelectedPrompt(prompt.name)}
className="ml-4"
>
- Edit
+ {intl.formatMessage(i18n.edit)}
))}
diff --git a/ui/desktop/src/components/settings/SettingsView.tsx b/ui/desktop/src/components/settings/SettingsView.tsx
index 181b3d40..fcdf9544 100644
--- a/ui/desktop/src/components/settings/SettingsView.tsx
+++ b/ui/desktop/src/components/settings/SettingsView.tsx
@@ -20,6 +20,42 @@ import LocalInferenceSection from './localInference/LocalInferenceSection';
import { CONFIGURATION_ENABLED } from '../../updates';
import { trackSettingsTabViewed } from '../../utils/analytics';
import { useFeatures } from '../../contexts/FeaturesContext';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'settingsView.title',
+ defaultMessage: 'Settings',
+ },
+ tabModels: {
+ id: 'settingsView.tabModels',
+ defaultMessage: 'Models',
+ },
+ tabLocalInference: {
+ id: 'settingsView.tabLocalInference',
+ defaultMessage: 'Local Inference',
+ },
+ tabChat: {
+ id: 'settingsView.tabChat',
+ defaultMessage: 'Chat',
+ },
+ tabSession: {
+ id: 'settingsView.tabSession',
+ defaultMessage: 'Session',
+ },
+ tabPrompts: {
+ id: 'settingsView.tabPrompts',
+ defaultMessage: 'Prompts',
+ },
+ tabKeyboard: {
+ id: 'settingsView.tabKeyboard',
+ defaultMessage: 'Keyboard',
+ },
+ tabApp: {
+ id: 'settingsView.tabApp',
+ defaultMessage: 'App',
+ },
+});
export type SettingsViewOptions = {
deepLinkConfig?: ExtensionConfig;
@@ -41,6 +77,7 @@ export default function SettingsView({
const [tunnelDisabled, setTunnelDisabled] = useState(false);
const hasTrackedInitialTab = useRef(false);
const { localInference } = useFeatures();
+ const intl = useIntl();
const handleTabChange = (tab: string) => {
setActiveTab(tab);
@@ -118,7 +155,7 @@ export default function SettingsView({
-
Settings
+ {intl.formatMessage(i18n.title)}
@@ -137,7 +174,7 @@ export default function SettingsView({
data-testid="settings-models-tab"
>
- Models
+ {intl.formatMessage(i18n.tabModels)}
{localInference && (
- Local Inference
+ {intl.formatMessage(i18n.tabLocalInference)}
)}
- Chat
+ {intl.formatMessage(i18n.tabChat)}
- Session
+ {intl.formatMessage(i18n.tabSession)}
- Prompts
+ {intl.formatMessage(i18n.tabPrompts)}
- Keyboard
+ {intl.formatMessage(i18n.tabKeyboard)}
- App
+ {intl.formatMessage(i18n.tabApp)}
diff --git a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx
index 5e56d80a..4d64898d 100644
--- a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx
+++ b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from 'react';
+import { defineMessages, useIntl } from '../../../i18n';
import { Switch } from '../../ui/switch';
import { Button } from '../../ui/button';
import { Settings, ChevronDown, ChevronUp } from 'lucide-react';
@@ -18,6 +19,50 @@ import { NavigationPositionSelector } from './NavigationPositionSelector';
import { NavigationCustomizationSettings } from './NavigationCustomizationSettings';
import { NavigationProvider, useNavigationContextSafe } from '../../Layout/NavigationContext';
+const i18n = defineMessages({
+ appearanceTitle: { id: 'settings.appearance.title', defaultMessage: 'Appearance' },
+ appearanceDesc: { id: 'settings.appearance.description', defaultMessage: 'Configure how goose appears on your system' },
+ notifications: { id: 'settings.notifications.title', defaultMessage: 'Notifications' },
+ notificationsDesc: { id: 'settings.notifications.description', defaultMessage: 'Notifications are managed by your OS - {link}' },
+ configGuide: { id: 'settings.notifications.configGuide', defaultMessage: 'Configuration guide' },
+ openSettings: { id: 'settings.notifications.openSettings', defaultMessage: 'Open Settings' },
+ menuBarIcon: { id: 'settings.menuBarIcon.title', defaultMessage: 'Menu bar icon' },
+ menuBarIconDesc: { id: 'settings.menuBarIcon.description', defaultMessage: 'Show goose in the menu bar' },
+ dockIcon: { id: 'settings.dockIcon.title', defaultMessage: 'Dock icon' },
+ dockIconDesc: { id: 'settings.dockIcon.description', defaultMessage: 'Show goose in the dock' },
+ preventSleep: { id: 'settings.preventSleep.title', defaultMessage: 'Prevent Sleep' },
+ preventSleepDesc: { id: 'settings.preventSleep.description', defaultMessage: 'Keep your computer awake while goose is running a task (screen can still lock)' },
+ costTracking: { id: 'settings.costTracking.title', defaultMessage: 'Cost Tracking' },
+ costTrackingDesc: { id: 'settings.costTracking.description', defaultMessage: 'Show model pricing and usage costs' },
+ themeTitle: { id: 'settings.theme.title', defaultMessage: 'Theme' },
+ themeDesc: { id: 'settings.theme.description', defaultMessage: 'Customize the look and feel of goose' },
+ navigationTitle: { id: 'settings.navigation.title', defaultMessage: 'Navigation' },
+ navigationDesc: { id: 'settings.navigation.description', defaultMessage: 'Customize navigation layout and behavior' },
+ navMode: { id: 'settings.navigation.mode', defaultMessage: 'Mode' },
+ navStyle: { id: 'settings.navigation.style', defaultMessage: 'Style' },
+ navPosition: { id: 'settings.navigation.position', defaultMessage: 'Position' },
+ navCustomize: { id: 'settings.navigation.customize', defaultMessage: 'Customize Items' },
+ helpTitle: { id: 'settings.help.title', defaultMessage: 'Help & feedback' },
+ helpDesc: { id: 'settings.help.description', defaultMessage: 'Help us improve goose by reporting issues or requesting new features' },
+ reportBug: { id: 'settings.help.reportBug', defaultMessage: 'Report a Bug' },
+ requestFeature: { id: 'settings.help.requestFeature', defaultMessage: 'Request a Feature' },
+ versionTitle: { id: 'settings.version.title', defaultMessage: 'Version' },
+ updatesTitle: { id: 'settings.updates.title', defaultMessage: 'Updates' },
+ updatesDesc: { id: 'settings.updates.description', defaultMessage: 'Check for and install updates to keep goose running at its best' },
+ notificationsModalTitle: { id: 'settings.notifications.modal.title', defaultMessage: 'How to Enable Notifications' },
+ notificationsMacInstructions: { id: 'settings.notifications.modal.macInstructions', defaultMessage: 'To enable notifications on macOS:' },
+ notificationsMacStep1: { id: 'settings.notifications.modal.macStep1', defaultMessage: 'Open System Preferences' },
+ notificationsMacStep2: { id: 'settings.notifications.modal.macStep2', defaultMessage: 'Click on Notifications' },
+ notificationsMacStep3: { id: 'settings.notifications.modal.macStep3', defaultMessage: 'Find and select goose in the application list' },
+ notificationsMacStep4: { id: 'settings.notifications.modal.macStep4', defaultMessage: 'Enable notifications and adjust settings as desired' },
+ notificationsWinInstructions: { id: 'settings.notifications.modal.winInstructions', defaultMessage: 'To enable notifications on Windows:' },
+ notificationsWinStep1: { id: 'settings.notifications.modal.winStep1', defaultMessage: 'Open Settings' },
+ notificationsWinStep2: { id: 'settings.notifications.modal.winStep2', defaultMessage: 'Go to System > Notifications' },
+ notificationsWinStep3: { id: 'settings.notifications.modal.winStep3', defaultMessage: 'Find and select goose in the application list' },
+ notificationsWinStep4: { id: 'settings.notifications.modal.winStep4', defaultMessage: 'Toggle notifications on and adjust settings as desired' },
+ close: { id: 'settings.close', defaultMessage: 'Close' },
+});
+
interface AppSettingsSectionProps {
scrollToSection?: string;
}
@@ -26,6 +71,7 @@ const NavigationSettingsContent: React.FC = () => {
const [isExpanded, setIsExpanded] = useState(false);
const navContext = useNavigationContextSafe();
const isOverlayMode = navContext?.navigationMode === 'overlay';
+ const intl = useIntl();
return (
@@ -35,8 +81,8 @@ const NavigationSettingsContent: React.FC = () => {
className="w-full flex items-center justify-between text-left"
>
- Navigation
- Customize navigation layout and behavior
+ {intl.formatMessage(i18n.navigationTitle)}
+ {intl.formatMessage(i18n.navigationDesc)}
{isExpanded ? (
@@ -48,23 +94,23 @@ const NavigationSettingsContent: React.FC = () => {
{isExpanded && (
-
Mode
+ {intl.formatMessage(i18n.navMode)}
{!isOverlayMode && (
-
Style
+ {intl.formatMessage(i18n.navStyle)}
)}
{!isOverlayMode && (
-
Position
+ {intl.formatMessage(i18n.navPosition)}
)}
-
Customize Items
+ {intl.formatMessage(i18n.navCustomize)}
@@ -209,25 +255,30 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
window.dispatchEvent(new CustomEvent('showPricingChanged'));
};
+ const intl = useIntl();
+
return (
- Appearance
- Configure how goose appears on your system
+ {intl.formatMessage(i18n.appearanceTitle)}
+ {intl.formatMessage(i18n.appearanceDesc)}
-
Notifications
+
{intl.formatMessage(i18n.notifications)}
- Notifications are managed by your OS{' - '}
- setShowNotificationModal(true)}
- >
- Configuration guide
-
+ {intl.formatMessage(i18n.notificationsDesc, {
+ link: (
+ setShowNotificationModal(true)}
+ >
+ {intl.formatMessage(i18n.configGuide)}
+
+ ),
+ })}
@@ -244,16 +295,16 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
}}
>
- Open Settings
+ {intl.formatMessage(i18n.openSettings)}
-
Menu bar icon
+
{intl.formatMessage(i18n.menuBarIcon)}
- Show goose in the menu bar
+ {intl.formatMessage(i18n.menuBarIconDesc)}
@@ -268,9 +319,9 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
{isMacOS && (
-
Dock icon
+
{intl.formatMessage(i18n.dockIcon)}
- Show goose in the dock
+ {intl.formatMessage(i18n.dockIconDesc)}
@@ -287,9 +338,9 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
{/* Prevent Sleep */}
-
Prevent Sleep
+
{intl.formatMessage(i18n.preventSleep)}
- Keep your computer awake while goose is running a task (screen can still lock)
+ {intl.formatMessage(i18n.preventSleepDesc)}
@@ -305,9 +356,9 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
{COST_TRACKING_ENABLED && (
-
Cost Tracking
+
{intl.formatMessage(i18n.costTracking)}
- Show model pricing and usage costs
+ {intl.formatMessage(i18n.costTrackingDesc)}
@@ -324,8 +375,8 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
- Theme
- Customize the look and feel of goose
+ {intl.formatMessage(i18n.themeTitle)}
+ {intl.formatMessage(i18n.themeDesc)}
@@ -339,9 +390,9 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
- Help & feedback
+ {intl.formatMessage(i18n.helpTitle)}
- Help us improve goose by reporting issues or requesting new features
+ {intl.formatMessage(i18n.helpDesc)}
@@ -356,7 +407,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
variant="secondary"
size="sm"
>
- Report a Bug
+ {intl.formatMessage(i18n.reportBug)}
{
@@ -368,7 +419,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
variant="secondary"
size="sm"
>
- Request a Feature
+ {intl.formatMessage(i18n.requestFeature)}
@@ -378,7 +429,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
{!shouldShowUpdates && (
- Version
+ {intl.formatMessage(i18n.versionTitle)}
@@ -400,9 +451,9 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
- Updates
+ {intl.formatMessage(i18n.updatesTitle)}
- Check for and install updates to keep goose running at its best
+ {intl.formatMessage(i18n.updatesDesc)}
@@ -421,7 +472,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
- How to Enable Notifications
+ {intl.formatMessage(i18n.notificationsModalTitle)}
@@ -429,22 +480,22 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
{/* OS-specific instructions */}
{isMacOS ? (
-
To enable notifications on macOS:
+
{intl.formatMessage(i18n.notificationsMacInstructions)}
- Open System Preferences
- Click on Notifications
- Find and select goose in the application list
- Enable notifications and adjust settings as desired
+ {intl.formatMessage(i18n.notificationsMacStep1)}
+ {intl.formatMessage(i18n.notificationsMacStep2)}
+ {intl.formatMessage(i18n.notificationsMacStep3)}
+ {intl.formatMessage(i18n.notificationsMacStep4)}
) : (
-
To enable notifications on Windows:
+
{intl.formatMessage(i18n.notificationsWinInstructions)}
- Open Settings
- Go to System > Notifications
- Find and select goose in the application list
- Toggle notifications on and adjust settings as desired
+ {intl.formatMessage(i18n.notificationsWinStep1)}
+ {intl.formatMessage(i18n.notificationsWinStep2)}
+ {intl.formatMessage(i18n.notificationsWinStep3)}
+ {intl.formatMessage(i18n.notificationsWinStep4)}
)}
@@ -452,7 +503,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
setShowNotificationModal(false)}>
- Close
+ {intl.formatMessage(i18n.close)}
diff --git a/ui/desktop/src/components/settings/app/ExternalBackendSection.tsx b/ui/desktop/src/components/settings/app/ExternalBackendSection.tsx
index 6d313d1f..deeff84b 100644
--- a/ui/desktop/src/components/settings/app/ExternalBackendSection.tsx
+++ b/ui/desktop/src/components/settings/app/ExternalBackendSection.tsx
@@ -5,8 +5,59 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../..
import { AlertCircle } from 'lucide-react';
import { ExternalGoosedConfig, defaultSettings } from '../../../utils/settings';
import { WEB_PROTOCOLS } from '../../../utils/urlSecurity';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'externalBackendSection.title',
+ defaultMessage: 'Goose Server',
+ },
+ description: {
+ id: 'externalBackendSection.description',
+ defaultMessage:
+ 'By default goose launches a server for you, use this to connect to an external goose server',
+ },
+ useExternalServer: {
+ id: 'externalBackendSection.useExternalServer',
+ defaultMessage: 'Use external server',
+ },
+ useExternalServerDescription: {
+ id: 'externalBackendSection.useExternalServerDescription',
+ defaultMessage: 'Connect to a goose server running elsewhere (requires app restart)',
+ },
+ serverUrl: {
+ id: 'externalBackendSection.serverUrl',
+ defaultMessage: 'Server URL',
+ },
+ secretKey: {
+ id: 'externalBackendSection.secretKey',
+ defaultMessage: 'Secret Key',
+ },
+ secretKeyPlaceholder: {
+ id: 'externalBackendSection.secretKeyPlaceholder',
+ defaultMessage: "Enter the server's secret key",
+ },
+ secretKeyHelp: {
+ id: 'externalBackendSection.secretKeyHelp',
+ defaultMessage: 'The secret key configured on the goosed server (GOOSE_SERVER__SECRET_KEY)',
+ },
+ restartNote: {
+ id: 'externalBackendSection.restartNote',
+ defaultMessage:
+ 'Changes require restarting Goose to take effect. New chat windows will connect to the external server.',
+ },
+ urlProtocolError: {
+ id: 'externalBackendSection.urlProtocolError',
+ defaultMessage: 'URL must use http or https protocol',
+ },
+ urlFormatError: {
+ id: 'externalBackendSection.urlFormatError',
+ defaultMessage: 'Invalid URL format',
+ },
+});
export default function ExternalBackendSection() {
+ const intl = useIntl();
const [config, setConfig] = useState(defaultSettings.externalGoosed);
const [isSaving, setIsSaving] = useState(false);
const [urlError, setUrlError] = useState(null);
@@ -27,13 +78,13 @@ export default function ExternalBackendSection() {
try {
const parsed = new URL(value);
if (!WEB_PROTOCOLS.includes(parsed.protocol)) {
- setUrlError('URL must use http or https protocol');
+ setUrlError(intl.formatMessage(i18n.urlProtocolError));
return false;
}
setUrlError(null);
return true;
} catch {
- setUrlError('Invalid URL format');
+ setUrlError(intl.formatMessage(i18n.urlFormatError));
return false;
}
};
@@ -73,18 +124,17 @@ export default function ExternalBackendSection() {
- Goose Server
+ {intl.formatMessage(i18n.title)}
- By default goose launches a server for you, use this to connect to an external goose
- server
+ {intl.formatMessage(i18n.description)}
-
Use external server
+
{intl.formatMessage(i18n.useExternalServer)}
- Connect to a goose server running elsewhere (requires app restart)
+ {intl.formatMessage(i18n.useExternalServerDescription)}
@@ -101,7 +151,7 @@ export default function ExternalBackendSection() {
<>
- Note: Changes require restarting Goose to take effect. New chat
- windows will connect to the external server.
+ Note: {intl.formatMessage(i18n.restartNote)}
>
diff --git a/ui/desktop/src/components/settings/app/NavigationCustomizationSettings.tsx b/ui/desktop/src/components/settings/app/NavigationCustomizationSettings.tsx
index a7f19f2a..c5299861 100644
--- a/ui/desktop/src/components/settings/app/NavigationCustomizationSettings.tsx
+++ b/ui/desktop/src/components/settings/app/NavigationCustomizationSettings.tsx
@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { GripVertical, Eye, EyeOff } from 'lucide-react';
+import { defineMessages, useIntl } from '../../../i18n';
import {
useNavigationContext,
DEFAULT_ITEM_ORDER,
@@ -7,14 +8,61 @@ import {
} from '../../Layout/NavigationContext';
import { cn } from '../../../utils';
-const ITEM_LABELS: Record
= {
- home: 'Home',
- chat: 'Chat',
- recipes: 'Recipes',
- apps: 'Apps',
- scheduler: 'Scheduler',
- extensions: 'Extensions',
- settings: 'Settings',
+const i18n = defineMessages({
+ dragInstructions: {
+ id: 'navigationCustomization.dragInstructions',
+ defaultMessage: 'Drag to reorder, click the eye icon to show/hide items',
+ },
+ resetToDefaults: {
+ id: 'navigationCustomization.resetToDefaults',
+ defaultMessage: 'Reset to defaults',
+ },
+ hideItem: {
+ id: 'navigationCustomization.hideItem',
+ defaultMessage: 'Hide item',
+ },
+ showItem: {
+ id: 'navigationCustomization.showItem',
+ defaultMessage: 'Show item',
+ },
+ itemHome: {
+ id: 'navigationCustomization.itemHome',
+ defaultMessage: 'Home',
+ },
+ itemChat: {
+ id: 'navigationCustomization.itemChat',
+ defaultMessage: 'Chat',
+ },
+ itemRecipes: {
+ id: 'navigationCustomization.itemRecipes',
+ defaultMessage: 'Recipes',
+ },
+ itemApps: {
+ id: 'navigationCustomization.itemApps',
+ defaultMessage: 'Apps',
+ },
+ itemScheduler: {
+ id: 'navigationCustomization.itemScheduler',
+ defaultMessage: 'Scheduler',
+ },
+ itemExtensions: {
+ id: 'navigationCustomization.itemExtensions',
+ defaultMessage: 'Extensions',
+ },
+ itemSettings: {
+ id: 'navigationCustomization.itemSettings',
+ defaultMessage: 'Settings',
+ },
+});
+
+const ITEM_LABEL_KEYS: Record = {
+ home: 'itemHome',
+ chat: 'itemChat',
+ recipes: 'itemRecipes',
+ apps: 'itemApps',
+ scheduler: 'itemScheduler',
+ extensions: 'itemExtensions',
+ settings: 'itemSettings',
};
interface NavigationCustomizationSettingsProps {
@@ -27,6 +75,7 @@ export const NavigationCustomizationSettings: React.FC(null);
const [dragOverItem, setDragOverItem] = useState(null);
+ const intl = useIntl();
const handleDragStart = (e: React.DragEvent, itemId: string) => {
setDraggedItem(itemId);
@@ -85,18 +134,26 @@ export const NavigationCustomizationSettings: React.FC {
+ const key = ITEM_LABEL_KEYS[itemId];
+ if (key) {
+ return intl.formatMessage(i18n[key]);
+ }
+ return itemId;
+ };
+
return (
- Drag to reorder, click the eye icon to show/hide items
+ {intl.formatMessage(i18n.dragInstructions)}
- Reset to defaults
+ {intl.formatMessage(i18n.resetToDefaults)}
@@ -104,7 +161,7 @@ export const NavigationCustomizationSettings: React.FC
toggleItemEnabled(itemId)}
className="p-1 rounded hover:bg-background-tertiary transition-colors flex-shrink-0"
- title={isEnabled ? 'Hide item' : 'Show item'}
+ title={isEnabled ? intl.formatMessage(i18n.hideItem) : intl.formatMessage(i18n.showItem)}
>
{isEnabled ? (
diff --git a/ui/desktop/src/components/settings/app/NavigationModeSelector.tsx b/ui/desktop/src/components/settings/app/NavigationModeSelector.tsx
index c3e41d76..7206965e 100644
--- a/ui/desktop/src/components/settings/app/NavigationModeSelector.tsx
+++ b/ui/desktop/src/components/settings/app/NavigationModeSelector.tsx
@@ -1,34 +1,55 @@
import React from 'react';
import { Columns2, Layers } from 'lucide-react';
+import { defineMessages, useIntl } from '../../../i18n';
import { useNavigationContext, NavigationMode } from '../../Layout/NavigationContext';
import { cn } from '../../../utils';
+const i18n = defineMessages({
+ pushLabel: {
+ id: 'navigationModeSelector.pushLabel',
+ defaultMessage: 'Push',
+ },
+ pushDescription: {
+ id: 'navigationModeSelector.pushDescription',
+ defaultMessage: 'Navigation pushes content',
+ },
+ overlayLabel: {
+ id: 'navigationModeSelector.overlayLabel',
+ defaultMessage: 'Overlay',
+ },
+ overlayDescription: {
+ id: 'navigationModeSelector.overlayDescription',
+ defaultMessage: 'Full-screen overlay',
+ },
+});
+
interface NavigationModeSelectorProps {
className?: string;
}
-const modes: {
- value: NavigationMode;
- label: string;
- icon: React.ReactNode;
- description: string;
-}[] = [
- {
- value: 'push',
- label: 'Push',
- icon: ,
- description: 'Navigation pushes content',
- },
- {
- value: 'overlay',
- label: 'Overlay',
- icon: ,
- description: 'Full-screen overlay',
- },
-];
-
export const NavigationModeSelector: React.FC = ({ className }) => {
const { navigationMode, setNavigationMode } = useNavigationContext();
+ const intl = useIntl();
+
+ const modes: {
+ value: NavigationMode;
+ label: string;
+ icon: React.ReactNode;
+ description: string;
+ }[] = [
+ {
+ value: 'push',
+ label: intl.formatMessage(i18n.pushLabel),
+ icon: ,
+ description: intl.formatMessage(i18n.pushDescription),
+ },
+ {
+ value: 'overlay',
+ label: intl.formatMessage(i18n.overlayLabel),
+ icon: ,
+ description: intl.formatMessage(i18n.overlayDescription),
+ },
+ ];
return (
diff --git a/ui/desktop/src/components/settings/app/NavigationPositionSelector.tsx b/ui/desktop/src/components/settings/app/NavigationPositionSelector.tsx
index fe2c2716..fee46868 100644
--- a/ui/desktop/src/components/settings/app/NavigationPositionSelector.tsx
+++ b/ui/desktop/src/components/settings/app/NavigationPositionSelector.tsx
@@ -1,23 +1,44 @@
import React from 'react';
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from 'lucide-react';
+import { defineMessages, useIntl } from '../../../i18n';
import { useNavigationContext, NavigationPosition } from '../../Layout/NavigationContext';
import { cn } from '../../../utils';
+const i18n = defineMessages({
+ topLabel: {
+ id: 'navigationPositionSelector.topLabel',
+ defaultMessage: 'Top',
+ },
+ bottomLabel: {
+ id: 'navigationPositionSelector.bottomLabel',
+ defaultMessage: 'Bottom',
+ },
+ leftLabel: {
+ id: 'navigationPositionSelector.leftLabel',
+ defaultMessage: 'Left',
+ },
+ rightLabel: {
+ id: 'navigationPositionSelector.rightLabel',
+ defaultMessage: 'Right',
+ },
+});
+
interface NavigationPositionSelectorProps {
className?: string;
}
-const positions: { value: NavigationPosition; label: string; icon: React.ReactNode }[] = [
- { value: 'top', label: 'Top', icon:
},
- { value: 'bottom', label: 'Bottom', icon:
},
- { value: 'left', label: 'Left', icon:
},
- { value: 'right', label: 'Right', icon:
},
-];
-
export const NavigationPositionSelector: React.FC
= ({
className,
}) => {
const { navigationPosition, setNavigationPosition } = useNavigationContext();
+ const intl = useIntl();
+
+ const positions: { value: NavigationPosition; label: string; icon: React.ReactNode }[] = [
+ { value: 'top', label: intl.formatMessage(i18n.topLabel), icon: },
+ { value: 'bottom', label: intl.formatMessage(i18n.bottomLabel), icon: },
+ { value: 'left', label: intl.formatMessage(i18n.leftLabel), icon: },
+ { value: 'right', label: intl.formatMessage(i18n.rightLabel), icon: },
+ ];
return (
diff --git a/ui/desktop/src/components/settings/app/NavigationStyleSelector.tsx b/ui/desktop/src/components/settings/app/NavigationStyleSelector.tsx
index f44dd996..cd5b79c3 100644
--- a/ui/desktop/src/components/settings/app/NavigationStyleSelector.tsx
+++ b/ui/desktop/src/components/settings/app/NavigationStyleSelector.tsx
@@ -1,34 +1,55 @@
import React from 'react';
import { LayoutGrid, List } from 'lucide-react';
+import { defineMessages, useIntl } from '../../../i18n';
import { useNavigationContext, NavigationStyle } from '../../Layout/NavigationContext';
import { cn } from '../../../utils';
+const i18n = defineMessages({
+ tileLabel: {
+ id: 'navigationStyleSelector.tileLabel',
+ defaultMessage: 'Tile',
+ },
+ tileDescription: {
+ id: 'navigationStyleSelector.tileDescription',
+ defaultMessage: 'Enlarged tile view',
+ },
+ listLabel: {
+ id: 'navigationStyleSelector.listLabel',
+ defaultMessage: 'List',
+ },
+ listDescription: {
+ id: 'navigationStyleSelector.listDescription',
+ defaultMessage: 'Classic condensed view',
+ },
+});
+
interface NavigationStyleSelectorProps {
className?: string;
}
-const styles: {
- value: NavigationStyle;
- label: string;
- icon: React.ReactNode;
- description: string;
-}[] = [
- {
- value: 'expanded',
- label: 'Tile',
- icon:
,
- description: 'Enlarged tile view',
- },
- {
- value: 'condensed',
- label: 'List',
- icon:
,
- description: 'Classic condensed view',
- },
-];
-
export const NavigationStyleSelector: React.FC
= ({ className }) => {
const { navigationStyle, setNavigationStyle } = useNavigationContext();
+ const intl = useIntl();
+
+ const styles: {
+ value: NavigationStyle;
+ label: string;
+ icon: React.ReactNode;
+ description: string;
+ }[] = [
+ {
+ value: 'expanded',
+ label: intl.formatMessage(i18n.tileLabel),
+ icon: ,
+ description: intl.formatMessage(i18n.tileDescription),
+ },
+ {
+ value: 'condensed',
+ label: intl.formatMessage(i18n.listLabel),
+ icon:
,
+ description: intl.formatMessage(i18n.listDescription),
+ },
+ ];
return (
diff --git a/ui/desktop/src/components/settings/app/TelemetrySettings.tsx b/ui/desktop/src/components/settings/app/TelemetrySettings.tsx
index 376b4f72..f39af433 100644
--- a/ui/desktop/src/components/settings/app/TelemetrySettings.tsx
+++ b/ui/desktop/src/components/settings/app/TelemetrySettings.tsx
@@ -9,6 +9,42 @@ import {
setTelemetryEnabled as setAnalyticsTelemetryEnabled,
trackTelemetryPreference,
} from '../../../utils/analytics';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'telemetrySettings.title',
+ defaultMessage: 'Privacy',
+ },
+ description: {
+ id: 'telemetrySettings.description',
+ defaultMessage: 'Control how your data is used',
+ },
+ toggleLabel: {
+ id: 'telemetrySettings.toggleLabel',
+ defaultMessage: 'Anonymous usage data',
+ },
+ toggleDescription: {
+ id: 'telemetrySettings.toggleDescription',
+ defaultMessage: 'Help improve goose by sharing anonymous usage statistics.',
+ },
+ learnMore: {
+ id: 'telemetrySettings.learnMore',
+ defaultMessage: 'Learn more',
+ },
+ configErrorTitle: {
+ id: 'telemetrySettings.configErrorTitle',
+ defaultMessage: 'Configuration Error',
+ },
+ loadError: {
+ id: 'telemetrySettings.loadError',
+ defaultMessage: 'Failed to load telemetry settings.',
+ },
+ updateError: {
+ id: 'telemetrySettings.updateError',
+ defaultMessage: 'Failed to update telemetry settings.',
+ },
+});
const TELEMETRY_CONFIG_KEY = 'GOOSE_TELEMETRY_ENABLED';
@@ -17,6 +53,7 @@ interface TelemetrySettingsProps {
}
export default function TelemetrySettings({ isWelcome = false }: TelemetrySettingsProps) {
+ const intl = useIntl();
const { read, upsert } = useConfig();
const [telemetryEnabled, setTelemetryEnabled] = useState(true);
const [isLoading, setIsLoading] = useState(true);
@@ -29,14 +66,14 @@ export default function TelemetrySettings({ isWelcome = false }: TelemetrySettin
} catch (error) {
console.error('Failed to load telemetry status:', error);
toastService.error({
- title: 'Configuration Error',
- msg: 'Failed to load telemetry settings.',
+ title: intl.formatMessage(i18n.configErrorTitle),
+ msg: intl.formatMessage(i18n.loadError),
traceback: error instanceof Error ? error.stack || '' : '',
});
} finally {
setIsLoading(false);
}
- }, [read]);
+ }, [read, intl]);
useEffect(() => {
loadTelemetryStatus();
@@ -51,8 +88,8 @@ export default function TelemetrySettings({ isWelcome = false }: TelemetrySettin
} catch (error) {
console.error('Failed to update telemetry status:', error);
toastService.error({
- title: 'Configuration Error',
- msg: 'Failed to update telemetry settings.',
+ title: intl.formatMessage(i18n.configErrorTitle),
+ msg: intl.formatMessage(i18n.updateError),
traceback: error instanceof Error ? error.stack || '' : '',
});
}
@@ -67,17 +104,17 @@ export default function TelemetrySettings({ isWelcome = false }: TelemetrySettin
return null;
}
- const title = 'Privacy';
- const description = 'Control how your data is used';
- const toggleLabel = 'Anonymous usage data';
- const toggleDescription = 'Help improve goose by sharing anonymous usage statistics.';
+ const title = intl.formatMessage(i18n.title);
+ const description = intl.formatMessage(i18n.description);
+ const toggleLabel = intl.formatMessage(i18n.toggleLabel);
+ const toggleDescription = intl.formatMessage(i18n.toggleDescription);
const learnMoreLink = (
setShowModal(true)}
className="text-blue-600 dark:text-blue-400 hover:underline"
>
- Learn more
+ {intl.formatMessage(i18n.learnMore)}
);
diff --git a/ui/desktop/src/components/settings/app/UpdateSection.tsx b/ui/desktop/src/components/settings/app/UpdateSection.tsx
index 21686739..37e9dd17 100644
--- a/ui/desktop/src/components/settings/app/UpdateSection.tsx
+++ b/ui/desktop/src/components/settings/app/UpdateSection.tsx
@@ -2,6 +2,90 @@ import React, { useState, useEffect } from 'react';
import { Button } from '../../ui/button';
import { Loader2, Download, CheckCircle, AlertCircle } from 'lucide-react';
import { errorMessage } from '../../../utils/conversionUtils';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ loading: {
+ id: 'updateSection.loading',
+ defaultMessage: 'Loading...',
+ },
+ currentVersion: {
+ id: 'updateSection.currentVersion',
+ defaultMessage: 'Current version',
+ },
+ versionAvailable: {
+ id: 'updateSection.versionAvailable',
+ defaultMessage: '→ {version} available',
+ },
+ upToDate: {
+ id: 'updateSection.upToDate',
+ defaultMessage: '(up to date)',
+ },
+ checkForUpdates: {
+ id: 'updateSection.checkForUpdates',
+ defaultMessage: 'Check for Updates',
+ },
+ installAndRestart: {
+ id: 'updateSection.installAndRestart',
+ defaultMessage: 'Install & Restart',
+ },
+ checking: {
+ id: 'updateSection.checking',
+ defaultMessage: 'Checking for updates...',
+ },
+ downloadingProgress: {
+ id: 'updateSection.downloadingProgress',
+ defaultMessage: 'Downloading update... {percent}%',
+ },
+ downloadReady: {
+ id: 'updateSection.downloadReady',
+ defaultMessage: 'Update downloaded and ready to install!',
+ },
+ latestVersion: {
+ id: 'updateSection.latestVersion',
+ defaultMessage: 'You are running the latest version!',
+ },
+ updateAvailable: {
+ id: 'updateSection.updateAvailable',
+ defaultMessage: 'Update available!',
+ },
+ versionIsAvailable: {
+ id: 'updateSection.versionIsAvailable',
+ defaultMessage: 'Version {version} is available',
+ },
+ downloadingUpdate: {
+ id: 'updateSection.downloadingUpdate',
+ defaultMessage: 'Downloading update...',
+ },
+ autoDownload: {
+ id: 'updateSection.autoDownload',
+ defaultMessage: 'Update will be downloaded automatically in the background.',
+ },
+ manualInstallNote: {
+ id: 'updateSection.manualInstallNote',
+ defaultMessage: "After download, you'll need to manually install the update.",
+ },
+ autoInstallNote: {
+ id: 'updateSection.autoInstallNote',
+ defaultMessage: 'The update will be installed automatically when you quit the app.',
+ },
+ readyInstallManual: {
+ id: 'updateSection.readyInstallManual',
+ defaultMessage: '✓ Update is ready! Click "Install & Restart" for installation instructions.',
+ },
+ manualInstallRequired: {
+ id: 'updateSection.manualInstallRequired',
+ defaultMessage: 'Manual installation required for this update method.',
+ },
+ readyInstallAuto: {
+ id: 'updateSection.readyInstallAuto',
+ defaultMessage: '✓ Update is ready! It will be installed when you quit Goose.',
+ },
+ installNowHint: {
+ id: 'updateSection.installNowHint',
+ defaultMessage: 'Or click "Install & Restart" to update now.',
+ },
+});
type UpdateStatus =
| 'idle'
@@ -25,6 +109,7 @@ interface UpdateEventData {
}
export default function UpdateSection() {
+ const intl = useIntl();
const [updateStatus, setUpdateStatus] = useState
('idle');
const [updateInfo, setUpdateInfo] = useState({
currentVersion: '',
@@ -167,20 +252,20 @@ export default function UpdateSection() {
const getStatusMessage = () => {
switch (updateStatus) {
case 'checking':
- return 'Checking for updates...';
+ return intl.formatMessage(i18n.checking);
case 'downloading':
- return `Downloading update... ${Math.round(progress)}%`;
+ return intl.formatMessage(i18n.downloadingProgress, { percent: Math.round(progress) });
case 'ready':
- return 'Update downloaded and ready to install!';
+ return intl.formatMessage(i18n.downloadReady);
case 'success':
return updateInfo.isUpdateAvailable === false
- ? 'You are running the latest version!'
- : 'Update available!';
+ ? intl.formatMessage(i18n.latestVersion)
+ : intl.formatMessage(i18n.updateAvailable);
case 'error':
return updateInfo.error || 'An error occurred';
default:
if (updateInfo.isUpdateAvailable) {
- return `Version ${updateInfo.latestVersion} is available`;
+ return intl.formatMessage(i18n.versionIsAvailable, { version: updateInfo.latestVersion });
}
return '';
}
@@ -207,15 +292,15 @@ export default function UpdateSection() {
- {updateInfo.currentVersion || 'Loading...'}
+ {updateInfo.currentVersion || intl.formatMessage(i18n.loading)}
-
Current version
+
{intl.formatMessage(i18n.currentVersion)}
{updateInfo.latestVersion && updateInfo.isUpdateAvailable && (
-
→ {updateInfo.latestVersion} available
+
{intl.formatMessage(i18n.versionAvailable, { version: updateInfo.latestVersion })}
)}
{updateInfo.currentVersion && updateInfo.isUpdateAvailable === false && (
-
(up to date)
+
{intl.formatMessage(i18n.upToDate)}
)}
@@ -227,12 +312,12 @@ export default function UpdateSection() {
variant="secondary"
size="sm"
>
- Check for Updates
+ {intl.formatMessage(i18n.checkForUpdates)}
{updateStatus === 'ready' && (
- Install & Restart
+ {intl.formatMessage(i18n.installAndRestart)}
)}
@@ -247,7 +332,7 @@ export default function UpdateSection() {
{updateStatus === 'downloading' && (
- Downloading update...
+ {intl.formatMessage(i18n.downloadingUpdate)}
{progress}%
@@ -262,14 +347,14 @@ export default function UpdateSection() {
{/* Update information */}
{updateInfo.isUpdateAvailable && updateStatus === 'idle' && (
-
Update will be downloaded automatically in the background.
+
{intl.formatMessage(i18n.autoDownload)}
{isUsingGitHubFallback ? (
- After download, you'll need to manually install the update.
+ {intl.formatMessage(i18n.manualInstallNote)}
) : (
- The update will be installed automatically when you quit the app.
+ {intl.formatMessage(i18n.autoInstallNote)}
)}
@@ -280,19 +365,19 @@ export default function UpdateSection() {
{isUsingGitHubFallback ? (
<>
- ✓ Update is ready! Click "Install & Restart" for installation instructions.
+ {intl.formatMessage(i18n.readyInstallManual)}
- Manual installation required for this update method.
+ {intl.formatMessage(i18n.manualInstallRequired)}
>
) : (
<>
- ✓ Update is ready! It will be installed when you quit Goose.
+ {intl.formatMessage(i18n.readyInstallAuto)}
- Or click "Install & Restart" to update now.
+ {intl.formatMessage(i18n.installNowHint)}
>
)}
diff --git a/ui/desktop/src/components/settings/chat/ChatSettingsSection.tsx b/ui/desktop/src/components/settings/chat/ChatSettingsSection.tsx
index 7bce1d60..e4befdae 100644
--- a/ui/desktop/src/components/settings/chat/ChatSettingsSection.tsx
+++ b/ui/desktop/src/components/settings/chat/ChatSettingsSection.tsx
@@ -5,14 +5,36 @@ import { ResponseStylesSection } from '../response_styles/ResponseStylesSection'
import { GoosehintsSection } from './GoosehintsSection';
import { SpellcheckToggle } from './SpellcheckToggle';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ modeTitle: {
+ id: 'chatSettings.modeTitle',
+ defaultMessage: 'Mode',
+ },
+ modeDescription: {
+ id: 'chatSettings.modeDescription',
+ defaultMessage: 'Configure how Goose interacts with tools and extensions',
+ },
+ responseStylesTitle: {
+ id: 'chatSettings.responseStylesTitle',
+ defaultMessage: 'Response Styles',
+ },
+ responseStylesDescription: {
+ id: 'chatSettings.responseStylesDescription',
+ defaultMessage: 'Choose how Goose should format and style its responses',
+ },
+});
export default function ChatSettingsSection({ sessionId }: { sessionId?: string }) {
+ const intl = useIntl();
+
return (
- Mode
- Configure how Goose interacts with tools and extensions
+ {intl.formatMessage(i18n.modeTitle)}
+ {intl.formatMessage(i18n.modeDescription)}
@@ -34,8 +56,8 @@ export default function ChatSettingsSection({ sessionId }: { sessionId?: string
- Response Styles
- Choose how Goose should format and style its responses
+ {intl.formatMessage(i18n.responseStylesTitle)}
+ {intl.formatMessage(i18n.responseStylesDescription)}
diff --git a/ui/desktop/src/components/settings/chat/GoosehintsModal.tsx b/ui/desktop/src/components/settings/chat/GoosehintsModal.tsx
index 29eb0272..6255e2db 100644
--- a/ui/desktop/src/components/settings/chat/GoosehintsModal.tsx
+++ b/ui/desktop/src/components/settings/chat/GoosehintsModal.tsx
@@ -10,51 +10,143 @@ import {
DialogTitle,
} from '../../ui/dialog';
import { errorMessage } from '../../../utils/conversionUtils';
+import { defineMessages, useIntl } from '../../../i18n';
-const HelpText = () => (
-
-
- .goosehints is a text file used to provide additional context about your project and improve
- the communication with Goose.
-
-
- Please make sure Developer extension is enabled in the
- extensions page. This extension is required to use .goosehints. You'll need to restart your
- session for .goosehints updates to take effect.
-
-
- See{' '}
-
- window.open('https://block.github.io/goose/docs/guides/using-goosehints/', '_blank')
- }
- >
- using .goosehints
- {' '}
- for more information.
-
-
-);
+const i18n = defineMessages({
+ dialogTitle: {
+ id: 'goosehintsModal.dialogTitle',
+ defaultMessage: 'Configure Project Hints (.goosehints)',
+ },
+ dialogDescription: {
+ id: 'goosehintsModal.dialogDescription',
+ defaultMessage:
+ 'Provide additional context about your project to improve communication with Goose',
+ },
+ helpText1: {
+ id: 'goosehintsModal.helpText1',
+ defaultMessage:
+ '.goosehints is a text file used to provide additional context about your project and improve the communication with Goose.',
+ },
+ helpText2: {
+ id: 'goosehintsModal.helpText2',
+ defaultMessage:
+ "Please make sure {bold} extension is enabled in the extensions page. This extension is required to use .goosehints. You'll need to restart your session for .goosehints updates to take effect.",
+ },
+ helpText3: {
+ id: 'goosehintsModal.helpText3',
+ defaultMessage: 'See {link} for more information.',
+ },
+ helpTextLink: {
+ id: 'goosehintsModal.helpTextLink',
+ defaultMessage: 'using .goosehints',
+ },
+ errorReading: {
+ id: 'goosehintsModal.errorReading',
+ defaultMessage: 'Error reading .goosehints file: {error}',
+ },
+ fileFound: {
+ id: 'goosehintsModal.fileFound',
+ defaultMessage: '.goosehints file found at: {filePath}',
+ },
+ fileCreating: {
+ id: 'goosehintsModal.fileCreating',
+ defaultMessage: 'Creating new .goosehints file at: {filePath}',
+ },
+ placeholder: {
+ id: 'goosehintsModal.placeholder',
+ defaultMessage: 'Enter project hints here...',
+ },
+ savedSuccessfully: {
+ id: 'goosehintsModal.savedSuccessfully',
+ defaultMessage: 'Saved successfully',
+ },
+ close: {
+ id: 'goosehintsModal.close',
+ defaultMessage: 'Close',
+ },
+ saving: {
+ id: 'goosehintsModal.saving',
+ defaultMessage: 'Saving...',
+ },
+ save: {
+ id: 'goosehintsModal.save',
+ defaultMessage: 'Save',
+ },
+ failedToAccess: {
+ id: 'goosehintsModal.failedToAccess',
+ defaultMessage: 'Failed to access .goosehints file',
+ },
+ failedToSave: {
+ id: 'goosehintsModal.failedToSave',
+ defaultMessage: 'Failed to save .goosehints file',
+ },
+ developer: {
+ id: 'goosehintsModal.developer',
+ defaultMessage: 'Developer',
+ },
+});
-const ErrorDisplay = ({ error }: { error: Error }) => (
-
-
Error reading .goosehints file: {errorMessage(error)}
-
-);
+const HelpText = () => {
+ const intl = useIntl();
-const FileInfo = ({ filePath, found }: { filePath: string; found: boolean }) => (
-
- {found ? (
-
-
.goosehints file found at: {filePath}
+ return (
+
+
{intl.formatMessage(i18n.helpText1)}
+
+ {intl.formatMessage(i18n.helpText2, {
+ bold: {intl.formatMessage(i18n.developer)} ,
+ })}
+
+
+ {intl.formatMessage(i18n.helpText3, {
+ link: (
+
+ window.open(
+ 'https://block.github.io/goose/docs/guides/using-goosehints/',
+ '_blank'
+ )
+ }
+ >
+ {intl.formatMessage(i18n.helpTextLink)}
+
+ ),
+ })}
+
+
+ );
+};
+
+const ErrorDisplay = ({ error }: { error: Error }) => {
+ const intl = useIntl();
+
+ return (
+
+
+ {intl.formatMessage(i18n.errorReading, { error: errorMessage(error) })}
- ) : (
-
Creating new .goosehints file at: {filePath}
- )}
-
-);
+
+ );
+};
+
+const FileInfo = ({ filePath, found }: { filePath: string; found: boolean }) => {
+ const intl = useIntl();
+
+ return (
+
+ {found ? (
+
+ {' '}
+ {intl.formatMessage(i18n.fileFound, { filePath })}
+
+ ) : (
+
{intl.formatMessage(i18n.fileCreating, { filePath })}
+ )}
+
+ );
+};
const getGoosehintsFile = async (filePath: string) => await window.electron.readFile(filePath);
@@ -64,6 +156,7 @@ interface GoosehintsModalProps {
}
export const GoosehintsModal = ({ directory, setIsGoosehintsModalOpen }: GoosehintsModalProps) => {
+ const intl = useIntl();
const goosehintsFilePath = `${directory}/.goosehints`;
const [goosehintsFile, setGoosehintsFile] = useState
('');
const [goosehintsFileFound, setGoosehintsFileFound] = useState(false);
@@ -80,11 +173,11 @@ export const GoosehintsModal = ({ directory, setIsGoosehintsModalOpen }: Goosehi
setGoosehintsFileReadError(found && error ? error : '');
} catch (error) {
console.error('Error fetching .goosehints file:', error);
- setGoosehintsFileReadError('Failed to access .goosehints file');
+ setGoosehintsFileReadError(intl.formatMessage(i18n.failedToAccess));
}
};
if (directory) fetchGoosehintsFile();
- }, [directory, goosehintsFilePath]);
+ }, [directory, goosehintsFilePath, intl]);
const writeFile = async () => {
setIsSaving(true);
@@ -96,7 +189,7 @@ export const GoosehintsModal = ({ directory, setIsGoosehintsModalOpen }: Goosehi
setTimeout(() => setSaveSuccess(false), 3000);
} catch (error) {
console.error('Error writing .goosehints file:', error);
- setGoosehintsFileReadError('Failed to save .goosehints file');
+ setGoosehintsFileReadError(intl.formatMessage(i18n.failedToSave));
} finally {
setIsSaving(false);
}
@@ -106,10 +199,8 @@ export const GoosehintsModal = ({ directory, setIsGoosehintsModalOpen }: Goosehi
setIsGoosehintsModalOpen(open)}>
- Configure Project Hints (.goosehints)
-
- Provide additional context about your project to improve communication with Goose
-
+ {intl.formatMessage(i18n.dialogTitle)}
+ {intl.formatMessage(i18n.dialogDescription)}
@@ -125,7 +216,7 @@ export const GoosehintsModal = ({ directory, setIsGoosehintsModalOpen }: Goosehi
value={goosehintsFile}
className="w-full h-80 border rounded-md p-2 text-sm resize-none bg-background-primary text-text-primary border-border-primary focus:outline-none focus:ring-2 focus:ring-blue-500"
onChange={(event) => setGoosehintsFile(event.target.value)}
- placeholder="Enter project hints here..."
+ placeholder={intl.formatMessage(i18n.placeholder)}
/>
)}
@@ -136,14 +227,14 @@ export const GoosehintsModal = ({ directory, setIsGoosehintsModalOpen }: Goosehi
{saveSuccess && (
- Saved successfully
+ {intl.formatMessage(i18n.savedSuccessfully)}
)}
setIsGoosehintsModalOpen(false)}>
- Close
+ {intl.formatMessage(i18n.close)}
- {isSaving ? 'Saving...' : 'Save'}
+ {isSaving ? intl.formatMessage(i18n.saving) : intl.formatMessage(i18n.save)}
diff --git a/ui/desktop/src/components/settings/chat/GoosehintsSection.tsx b/ui/desktop/src/components/settings/chat/GoosehintsSection.tsx
index c84e44f1..7f99c2d9 100644
--- a/ui/desktop/src/components/settings/chat/GoosehintsSection.tsx
+++ b/ui/desktop/src/components/settings/chat/GoosehintsSection.tsx
@@ -2,8 +2,26 @@ import { useState } from 'react';
import { Button } from '../../ui/button';
import { FolderKey } from 'lucide-react';
import { GoosehintsModal } from './GoosehintsModal';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'goosehintsSection.title',
+ defaultMessage: 'Project Hints (.goosehints)',
+ },
+ description: {
+ id: 'goosehintsSection.description',
+ defaultMessage:
+ "Configure your project's .goosehints file to provide additional context to Goose",
+ },
+ configure: {
+ id: 'goosehintsSection.configure',
+ defaultMessage: 'Configure',
+ },
+});
export const GoosehintsSection = () => {
+ const intl = useIntl();
const [isModalOpen, setIsModalOpen] = useState(false);
const directory = window.appConfig?.get('GOOSE_WORKING_DIR') as string;
@@ -11,9 +29,9 @@ export const GoosehintsSection = () => {
<>
-
Project Hints (.goosehints)
+
{intl.formatMessage(i18n.title)}
- Configure your project's .goosehints file to provide additional context to Goose
+ {intl.formatMessage(i18n.description)}
{
className="flex items-center gap-2"
>
- Configure
+ {intl.formatMessage(i18n.configure)}
{isModalOpen && (
diff --git a/ui/desktop/src/components/settings/chat/SpellcheckToggle.tsx b/ui/desktop/src/components/settings/chat/SpellcheckToggle.tsx
index 73a4444f..ccc8c3df 100644
--- a/ui/desktop/src/components/settings/chat/SpellcheckToggle.tsx
+++ b/ui/desktop/src/components/settings/chat/SpellcheckToggle.tsx
@@ -1,7 +1,20 @@
import { useState, useEffect } from 'react';
import { Switch } from '../../ui/switch';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'spellcheckToggle.title',
+ defaultMessage: 'Enable Spellcheck',
+ },
+ description: {
+ id: 'spellcheckToggle.description',
+ defaultMessage: 'Check spelling in the chat input. Requires restart to take effect.',
+ },
+});
export const SpellcheckToggle = () => {
+ const intl = useIntl();
const [enabled, setEnabled] = useState(true);
useEffect(() => {
@@ -20,9 +33,9 @@ export const SpellcheckToggle = () => {
return (
-
Enable Spellcheck
+
{intl.formatMessage(i18n.title)}
- Check spelling in the chat input. Requires restart to take effect.
+ {intl.formatMessage(i18n.description)}
diff --git a/ui/desktop/src/components/settings/config/ConfigSettings.tsx b/ui/desktop/src/components/settings/config/ConfigSettings.tsx
index 00cb0579..4aa4cca3 100644
--- a/ui/desktop/src/components/settings/config/ConfigSettings.tsx
+++ b/ui/desktop/src/components/settings/config/ConfigSettings.tsx
@@ -18,8 +18,77 @@ import {
DialogTrigger,
} from '../../ui/dialog';
import { errorMessage } from '../../../utils/conversionUtils';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'configSettings.title',
+ defaultMessage: 'Configuration',
+ },
+ description: {
+ id: 'configSettings.description',
+ defaultMessage: 'Edit your goose configuration settings',
+ },
+ descriptionWithProvider: {
+ id: 'configSettings.descriptionWithProvider',
+ defaultMessage: 'Edit your goose configuration settings (current settings for {provider})',
+ },
+ editConfiguration: {
+ id: 'configSettings.editConfiguration',
+ defaultMessage: 'Edit Configuration',
+ },
+ configurationEditor: {
+ id: 'configSettings.configurationEditor',
+ defaultMessage: 'Configuration Editor',
+ },
+ noSettings: {
+ id: 'configSettings.noSettings',
+ defaultMessage: 'No configuration settings found.',
+ },
+ enterValue: {
+ id: 'configSettings.enterValue',
+ defaultMessage: 'Enter {name}',
+ },
+ saving: {
+ id: 'configSettings.saving',
+ defaultMessage: 'Saving...',
+ },
+ resetChanges: {
+ id: 'configSettings.resetChanges',
+ defaultMessage: 'Reset Changes',
+ },
+ done: {
+ id: 'configSettings.done',
+ defaultMessage: 'Done',
+ },
+ configUpdated: {
+ id: 'configSettings.configUpdated',
+ defaultMessage: 'Configuration Updated',
+ },
+ configUpdatedMsg: {
+ id: 'configSettings.configUpdatedMsg',
+ defaultMessage: 'Successfully saved "{name}"',
+ },
+ saveFailed: {
+ id: 'configSettings.saveFailed',
+ defaultMessage: 'Save Failed',
+ },
+ saveFailedMsg: {
+ id: 'configSettings.saveFailedMsg',
+ defaultMessage: 'Failed to save "{name}"',
+ },
+ configReset: {
+ id: 'configSettings.configReset',
+ defaultMessage: 'Configuration Reset',
+ },
+ configResetMsg: {
+ id: 'configSettings.configResetMsg',
+ defaultMessage: 'All changes have been reverted',
+ },
+});
export default function ConfigSettings() {
+ const intl = useIntl();
const { config, upsert } = useConfig();
const typedConfig = config as ConfigData;
const [configValues, setConfigValues] = useState
({});
@@ -70,8 +139,8 @@ export default function ConfigSettings() {
try {
await upsert(key, configValues[key], false);
toastSuccess({
- title: 'Configuration Updated',
- msg: `Successfully saved "${getUiNames(key)}"`,
+ title: intl.formatMessage(i18n.configUpdated),
+ msg: intl.formatMessage(i18n.configUpdatedMsg, { name: getUiNames(key) }),
});
// Remove this key from modified keys since it's now saved
@@ -83,8 +152,8 @@ export default function ConfigSettings() {
} catch (error) {
console.error('Failed to save config:', error);
toastError({
- title: 'Save Failed',
- msg: `Failed to save "${getUiNames(key)}"`,
+ title: intl.formatMessage(i18n.saveFailed),
+ msg: intl.formatMessage(i18n.saveFailedMsg, { name: getUiNames(key) }),
traceback: errorMessage(error),
});
} finally {
@@ -96,8 +165,8 @@ export default function ConfigSettings() {
setConfigValues(typedConfig);
setModifiedKeys(new Set());
toastSuccess({
- title: 'Configuration Reset',
- msg: 'All changes have been reverted',
+ title: intl.formatMessage(i18n.configReset),
+ msg: intl.formatMessage(i18n.configResetMsg),
});
};
@@ -141,11 +210,12 @@ export default function ConfigSettings() {
- Configuration
+ {intl.formatMessage(i18n.title)}
- Edit your goose configuration settings
- {currentProvider && ` (current settings for ${currentProvider})`}
+ {currentProvider
+ ? intl.formatMessage(i18n.descriptionWithProvider, { provider: currentProvider })
+ : intl.formatMessage(i18n.description)}
@@ -153,25 +223,26 @@ export default function ConfigSettings() {
- Edit Configuration
+ {intl.formatMessage(i18n.editConfiguration)}
- Configuration Editor
+ {intl.formatMessage(i18n.configurationEditor)}
- Edit your goose configuration settings
- {currentProvider && ` (current settings for ${currentProvider})`}
+ {currentProvider
+ ? intl.formatMessage(i18n.descriptionWithProvider, { provider: currentProvider })
+ : intl.formatMessage(i18n.description)}
{configEntries.length === 0 ? (
-
No configuration settings found.
+
{intl.formatMessage(i18n.noSettings)}
) : (
configEntries.map(([key, _value]) => (
@@ -185,7 +256,7 @@ export default function ConfigSettings() {
'text-text-primary border-border-primary hover:border-border-primary transition-colors',
modifiedKeys.has(key) && 'border-blue-500 focus:ring-blue-500/20'
)}
- placeholder={`Enter ${getUiNames(key)}`}
+ placeholder={intl.formatMessage(i18n.enterValue, { name: getUiNames(key) })}
/>
handleSave(key)}
@@ -195,7 +266,7 @@ export default function ConfigSettings() {
className="min-w-[60px]"
>
{saving === key ? (
- Saving...
+ {intl.formatMessage(i18n.saving)}
) : (
)}
@@ -210,11 +281,11 @@ export default function ConfigSettings() {
{modifiedKeys.size > 0 && (
- Reset Changes
+ {intl.formatMessage(i18n.resetChanges)}
)}
setIsModalOpen(false)} variant="default">
- Done
+ {intl.formatMessage(i18n.done)}
diff --git a/ui/desktop/src/components/settings/dictation/DictationSettings.tsx b/ui/desktop/src/components/settings/dictation/DictationSettings.tsx
index 3b73408a..94a02086 100644
--- a/ui/desktop/src/components/settings/dictation/DictationSettings.tsx
+++ b/ui/desktop/src/components/settings/dictation/DictationSettings.tsx
@@ -16,8 +16,73 @@ import {
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '../../ui/dropdown-menu';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ voiceDictationProvider: {
+ id: 'dictationSettings.voiceDictationProvider',
+ defaultMessage: 'Voice Dictation Provider',
+ },
+ chooseVoiceConversion: {
+ id: 'dictationSettings.chooseVoiceConversion',
+ defaultMessage: 'Choose how voice is converted to text',
+ },
+ disabled: {
+ id: 'dictationSettings.disabled',
+ defaultMessage: 'Disabled',
+ },
+ notConfigured: {
+ id: 'dictationSettings.notConfigured',
+ defaultMessage: '(not configured)',
+ },
+ configureApiKey: {
+ id: 'dictationSettings.configureApiKey',
+ defaultMessage: 'Configure the API key in {settingsPath} ',
+ },
+ configuredIn: {
+ id: 'dictationSettings.configuredIn',
+ defaultMessage: '✓ Configured in {settingsPath}',
+ },
+ apiKey: {
+ id: 'dictationSettings.apiKey',
+ defaultMessage: 'API Key',
+ },
+ requiredForTranscription: {
+ id: 'dictationSettings.requiredForTranscription',
+ defaultMessage: 'Required for transcription',
+ },
+ configured: {
+ id: 'dictationSettings.configured',
+ defaultMessage: '(Configured)',
+ },
+ updateApiKey: {
+ id: 'dictationSettings.updateApiKey',
+ defaultMessage: 'Update API Key',
+ },
+ addApiKey: {
+ id: 'dictationSettings.addApiKey',
+ defaultMessage: 'Add API Key',
+ },
+ removeApiKey: {
+ id: 'dictationSettings.removeApiKey',
+ defaultMessage: 'Remove API Key',
+ },
+ enterApiKey: {
+ id: 'dictationSettings.enterApiKey',
+ defaultMessage: 'Enter your API key',
+ },
+ save: {
+ id: 'dictationSettings.save',
+ defaultMessage: 'Save',
+ },
+ cancel: {
+ id: 'dictationSettings.cancel',
+ defaultMessage: 'Cancel',
+ },
+});
export const DictationSettings = () => {
+ const intl = useIntl();
const { localInference, isLoading: isFeaturesLoading } = useFeatures();
const [provider, setProvider] = useState(null);
const [providerStatuses, setProviderStatuses] = useState>(
@@ -110,7 +175,7 @@ export const DictationSettings = () => {
};
const getProviderLabel = (p: DictationProvider | null): string => {
- if (!p) return 'Disabled';
+ if (!p) return intl.formatMessage(i18n.disabled);
return p.charAt(0).toUpperCase() + p.slice(1);
};
@@ -122,9 +187,9 @@ export const DictationSettings = () => {
-
Voice Dictation Provider
+
{intl.formatMessage(i18n.voiceDictationProvider)}
- Choose how voice is converted to text
+ {intl.formatMessage(i18n.chooseVoiceConversion)}
open && refreshStatuses()}>
@@ -137,12 +202,12 @@ export const DictationSettings = () => {
value={provider ?? 'disabled'}
onValueChange={handleProviderChange}
>
- Disabled
+ {intl.formatMessage(i18n.disabled)}
{visibleProviders.map((p) => (
{getProviderLabel(p)}
{!providerStatuses[p]?.configured && (
- (not configured)
+ {intl.formatMessage(i18n.notConfigured)}
)}
))}
@@ -161,22 +226,22 @@ export const DictationSettings = () => {
{!providerStatuses[provider].configured ? (
- Configure the API key in {providerStatuses[provider].settings_path}
+ {intl.formatMessage(i18n.configureApiKey, { settingsPath: providerStatuses[provider].settings_path, b: (chunks: React.ReactNode) => {chunks} })}
) : (
- ✓ Configured in {providerStatuses[provider].settings_path}
+ {intl.formatMessage(i18n.configuredIn, { settingsPath: providerStatuses[provider].settings_path })}
)}
) : (
-
API Key
+
{intl.formatMessage(i18n.apiKey)}
- Required for transcription
+ {intl.formatMessage(i18n.requiredForTranscription)}
{providerStatuses[provider]?.configured && (
- (Configured)
+ {intl.formatMessage(i18n.configured)}
)}
@@ -184,11 +249,11 @@ export const DictationSettings = () => {
{!isEditingKey ? (
setIsEditingKey(true)}>
- {providerStatuses[provider]?.configured ? 'Update API Key' : 'Add API Key'}
+ {providerStatuses[provider]?.configured ? intl.formatMessage(i18n.updateApiKey) : intl.formatMessage(i18n.addApiKey)}
{providerStatuses[provider]?.configured && (
- Remove API Key
+ {intl.formatMessage(i18n.removeApiKey)}
)}
@@ -198,16 +263,16 @@ export const DictationSettings = () => {
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
- placeholder="Enter your API key"
+ placeholder={intl.formatMessage(i18n.enterApiKey)}
className="max-w-md"
autoFocus
/>
- Save
+ {intl.formatMessage(i18n.save)}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
diff --git a/ui/desktop/src/components/settings/dictation/LocalModelManager.tsx b/ui/desktop/src/components/settings/dictation/LocalModelManager.tsx
index 80c73f59..2169ef7c 100644
--- a/ui/desktop/src/components/settings/dictation/LocalModelManager.tsx
+++ b/ui/desktop/src/components/settings/dictation/LocalModelManager.tsx
@@ -11,6 +11,51 @@ import {
type WhisperModelResponse,
type DownloadProgress,
} from '../../../api';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ gpuAcceleration: {
+ id: 'localModelManager.gpuAcceleration',
+ defaultMessage:
+ 'Supports GPU acceleration (CUDA for NVIDIA, Metal for Apple Silicon). GPU features must be enabled at build time for hardware acceleration.',
+ },
+ recommended: {
+ id: 'localModelManager.recommended',
+ defaultMessage: 'Recommended',
+ },
+ active: {
+ id: 'localModelManager.active',
+ defaultMessage: 'Active',
+ },
+ recommendedForHardware: {
+ id: 'localModelManager.recommendedForHardware',
+ defaultMessage: 'Recommended for your hardware',
+ },
+ downloaded: {
+ id: 'localModelManager.downloaded',
+ defaultMessage: 'Downloaded',
+ },
+ download: {
+ id: 'localModelManager.download',
+ defaultMessage: 'Download',
+ },
+ deleteConfirm: {
+ id: 'localModelManager.deleteConfirm',
+ defaultMessage: 'Delete this model? You can re-download it later.',
+ },
+ showRecommendedOnly: {
+ id: 'localModelManager.showRecommendedOnly',
+ defaultMessage: 'Show recommended only',
+ },
+ showAllModels: {
+ id: 'localModelManager.showAllModels',
+ defaultMessage: 'Show all models ({count} more)',
+ },
+ noModels: {
+ id: 'localModelManager.noModels',
+ defaultMessage: 'No models available',
+ },
+});
const LOCAL_WHISPER_MODEL_CONFIG_KEY = 'LOCAL_WHISPER_MODEL';
@@ -26,6 +71,7 @@ const capitalize = (str: string): string => {
};
export const LocalModelManager = () => {
+ const intl = useIntl();
const [models, setModels] = useState([]);
const [downloads, setDownloads] = useState>(new Map());
const [selectedModelId, setSelectedModelId] = useState(null);
@@ -118,7 +164,7 @@ export const LocalModelManager = () => {
};
const deleteModel = async (modelId: string) => {
- if (!window.confirm('Delete this model? You can re-download it later.')) return;
+ if (!window.confirm(intl.formatMessage(i18n.deleteConfirm))) return;
try {
await deleteModelApi({ path: { model_id: modelId } });
@@ -144,8 +190,7 @@ export const LocalModelManager = () => {
- Supports GPU acceleration (CUDA for NVIDIA, Metal for Apple Silicon). GPU features must be
- enabled at build time for hardware acceleration.
+ {intl.formatMessage(i18n.gpuAcceleration)}
@@ -182,12 +227,12 @@ export const LocalModelManager = () => {
{model.size_mb}MB
{model.recommended && (
- Recommended
+ {intl.formatMessage(i18n.recommended)}
)}
{isSelected && (
- Active
+ {intl.formatMessage(i18n.active)}
)}
@@ -195,7 +240,7 @@ export const LocalModelManager = () => {
{model.description}
{model.recommended && (
- Recommended for your hardware
+ {intl.formatMessage(i18n.recommendedForHardware)}
)}
@@ -205,7 +250,7 @@ export const LocalModelManager = () => {
<>
- Downloaded
+ {intl.formatMessage(i18n.downloaded)}
{
) : (
startDownload(model.id)}>
- Download
+ {intl.formatMessage(i18n.download)}
)}
@@ -269,19 +314,19 @@ export const LocalModelManager = () => {
{showAllModels ? (
<>
- Show recommended only
+ {intl.formatMessage(i18n.showRecommendedOnly)}
>
) : (
<>
- Show all models ({models.length - displayedModels.length} more)
+ {intl.formatMessage(i18n.showAllModels, { count: models.length - displayedModels.length })}
>
)}
)}
{models.length === 0 && (
-
No models available
+
{intl.formatMessage(i18n.noModels)}
)}
);
diff --git a/ui/desktop/src/components/settings/dictation/MicrophoneSelector.tsx b/ui/desktop/src/components/settings/dictation/MicrophoneSelector.tsx
index 5d464cad..cdfa6c5f 100644
--- a/ui/desktop/src/components/settings/dictation/MicrophoneSelector.tsx
+++ b/ui/desktop/src/components/settings/dictation/MicrophoneSelector.tsx
@@ -8,6 +8,50 @@ import {
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '../../ui/dropdown-menu';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ microphone: {
+ id: 'microphoneSelector.microphone',
+ defaultMessage: 'Microphone',
+ },
+ grantAccessDescription: {
+ id: 'microphoneSelector.grantAccessDescription',
+ defaultMessage: 'Grant access to see available microphones',
+ },
+ grantAccess: {
+ id: 'microphoneSelector.grantAccess',
+ defaultMessage: 'Grant Access',
+ },
+ chooseDescription: {
+ id: 'microphoneSelector.chooseDescription',
+ defaultMessage: 'Choose which microphone to use for dictation',
+ },
+ systemDefault: {
+ id: 'microphoneSelector.systemDefault',
+ defaultMessage: 'System Default',
+ },
+ selectedMicrophone: {
+ id: 'microphoneSelector.selectedMicrophone',
+ defaultMessage: 'Selected Microphone',
+ },
+ microphoneLabel: {
+ id: 'microphoneSelector.microphoneLabel',
+ defaultMessage: 'Microphone {index}',
+ },
+ stop: {
+ id: 'microphoneSelector.stop',
+ defaultMessage: 'Stop',
+ },
+ test: {
+ id: 'microphoneSelector.test',
+ defaultMessage: 'Test',
+ },
+ speakToTest: {
+ id: 'microphoneSelector.speakToTest',
+ defaultMessage: 'Speak to test your microphone ({seconds}s)',
+ },
+});
interface MicrophoneSelectorProps {
selectedDeviceId: string | null;
@@ -20,6 +64,7 @@ export const MicrophoneSelector = ({
selectedDeviceId,
onDeviceChange,
}: MicrophoneSelectorProps) => {
+ const intl = useIntl();
const [devices, setDevices] = useState
([]);
const [hasPermission, setHasPermission] = useState(false);
const [isTesting, setIsTesting] = useState(false);
@@ -120,27 +165,27 @@ export const MicrophoneSelector = ({
}, [stopTest]);
const getDeviceLabel = (device: MediaDeviceInfo, index: number): string => {
- return device.label || `Microphone ${index + 1}`;
+ return device.label || intl.formatMessage(i18n.microphoneLabel, { index: index + 1 });
};
const selectedLabel = (): string => {
- if (!selectedDeviceId) return 'System Default';
+ if (!selectedDeviceId) return intl.formatMessage(i18n.systemDefault);
const device = devices.find((d) => d.deviceId === selectedDeviceId);
- if (device) return device.label || 'Selected Microphone';
- return 'System Default';
+ if (device) return device.label || intl.formatMessage(i18n.selectedMicrophone);
+ return intl.formatMessage(i18n.systemDefault);
};
if (!hasPermission) {
return (
-
Microphone
+
{intl.formatMessage(i18n.microphone)}
- Grant access to see available microphones
+ {intl.formatMessage(i18n.grantAccessDescription)}
- Grant Access
+ {intl.formatMessage(i18n.grantAccess)}
);
@@ -150,9 +195,9 @@ export const MicrophoneSelector = ({
-
Microphone
+
{intl.formatMessage(i18n.microphone)}
- Choose which microphone to use for dictation
+ {intl.formatMessage(i18n.chooseDescription)}
@@ -166,7 +211,7 @@ export const MicrophoneSelector = ({
value={selectedDeviceId ?? 'system_default'}
onValueChange={(v) => onDeviceChange(v === 'system_default' ? null : v)}
>
- System Default
+ {intl.formatMessage(i18n.systemDefault)}
{devices.map((device, i) => (
{getDeviceLabel(device, i)}
@@ -182,7 +227,7 @@ export const MicrophoneSelector = ({
className="shrink-0"
>
- {isTesting ? 'Stop' : 'Test'}
+ {isTesting ? intl.formatMessage(i18n.stop) : intl.formatMessage(i18n.test)}
@@ -196,7 +241,7 @@ export const MicrophoneSelector = ({
/>
- Speak to test your microphone ({Math.ceil(TEST_DURATION_MS / 1000)}s)
+ {intl.formatMessage(i18n.speakToTest, { seconds: Math.ceil(TEST_DURATION_MS / 1000) })}
)}
diff --git a/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx
index ef3b577c..ed41a686 100644
--- a/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx
+++ b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx
@@ -3,6 +3,7 @@ import { Button } from '../../ui/button';
import { Plus } from 'lucide-react';
import { GPSIcon } from '../../ui/icons';
import { useConfig, FixedExtensionEntry } from '../../ConfigContext';
+import { defineMessages, useIntl } from '../../../i18n';
import ExtensionList from './subcomponents/ExtensionList';
import ExtensionModal from './modal/ExtensionModal';
import {
@@ -15,6 +16,29 @@ import {
import { activateExtensionDefault, deleteExtension, toggleExtensionDefault } from './index';
import { ExtensionConfig } from '../../../api/types.gen';
+const i18n = defineMessages({
+ addCustomExtension: {
+ id: 'extensionsSection.addCustomExtension',
+ defaultMessage: 'Add custom extension',
+ },
+ browseExtensions: {
+ id: 'extensionsSection.browseExtensions',
+ defaultMessage: 'Browse extensions',
+ },
+ updateExtension: {
+ id: 'extensionsSection.updateExtension',
+ defaultMessage: 'Update Extension',
+ },
+ saveChanges: {
+ id: 'extensionsSection.saveChanges',
+ defaultMessage: 'Save Changes',
+ },
+ addExtension: {
+ id: 'extensionsSection.addExtension',
+ defaultMessage: 'Add Extension',
+ },
+});
+
interface ExtensionSectionProps {
deepLinkConfig?: ExtensionConfig;
showEnvVars?: boolean;
@@ -36,6 +60,7 @@ export default function ExtensionsSection({
onModalClose,
searchTerm = '',
}: ExtensionSectionProps) {
+ const intl = useIntl();
const { getExtensions, addExtension, removeExtension, extensionsList } = useConfig();
const [selectedExtension, setSelectedExtension] = useState
(null);
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -198,7 +223,7 @@ export default function ExtensionsSection({
onClick={() => setIsAddModalOpen(true)}
>
- Add custom extension
+ {intl.formatMessage(i18n.addCustomExtension)}
window.open('https://block.github.io/goose/v1/extensions/', '_blank')}
>
- Browse extensions
+ {intl.formatMessage(i18n.browseExtensions)}
)}
@@ -214,12 +239,12 @@ export default function ExtensionsSection({
{/* Modal for updating an existing extension */}
{isModalOpen && selectedExtension && (
)}
@@ -227,11 +252,11 @@ export default function ExtensionsSection({
{/* Modal for adding a new extension */}
{isAddModalOpen && (
)}
@@ -239,14 +264,14 @@ export default function ExtensionsSection({
{/* Modal for adding extension from deeplink*/}
{deepLinkConfigStateVar && showEnvVarsStateVar && (
)}
diff --git a/ui/desktop/src/components/settings/extensions/modal/EnvVarsSection.tsx b/ui/desktop/src/components/settings/extensions/modal/EnvVarsSection.tsx
index 35412fe6..b8945746 100644
--- a/ui/desktop/src/components/settings/extensions/modal/EnvVarsSection.tsx
+++ b/ui/desktop/src/components/settings/extensions/modal/EnvVarsSection.tsx
@@ -3,6 +3,38 @@ import { Button } from '../../../ui/button';
import { Plus, X, Edit } from 'lucide-react';
import { Input } from '../../../ui/input';
import { cn } from '../../../../utils';
+import { defineMessages, useIntl } from '../../../../i18n';
+
+const i18n = defineMessages({
+ environmentVariables: {
+ id: 'envVarsSection.environmentVariables',
+ defaultMessage: 'Environment Variables',
+ },
+ envVarsDescription: {
+ id: 'envVarsSection.envVarsDescription',
+ defaultMessage: 'Add key-value pairs for environment variables. Click the "+" button to add after filling both fields. For existing secret values, click the edit button to modify.',
+ },
+ variableName: {
+ id: 'envVarsSection.variableName',
+ defaultMessage: 'Variable name',
+ },
+ value: {
+ id: 'envVarsSection.value',
+ defaultMessage: 'Value',
+ },
+ bothRequired: {
+ id: 'envVarsSection.bothRequired',
+ defaultMessage: 'Both variable name and value must be entered',
+ },
+ noSpaces: {
+ id: 'envVarsSection.noSpaces',
+ defaultMessage: 'Variable name cannot contain spaces',
+ },
+ add: {
+ id: 'envVarsSection.add',
+ defaultMessage: 'Add',
+ },
+});
interface EnvVarsSectionProps {
envVars: { key: string; value: string; isEdited?: boolean }[];
@@ -21,6 +53,7 @@ export default function EnvVarsSection({
submitAttempted,
onPendingInputChange,
}: EnvVarsSectionProps) {
+ const intl = useIntl();
const [newKey, setNewKey] = React.useState('');
const [newValue, setNewValue] = React.useState('');
const [validationError, setValidationError] = React.useState(null);
@@ -45,7 +78,7 @@ export default function EnvVarsSection({
key: keyEmpty,
value: valueEmpty,
});
- setValidationError('Both variable name and value must be entered');
+ setValidationError(intl.formatMessage(i18n.bothRequired));
return;
}
@@ -54,7 +87,7 @@ export default function EnvVarsSection({
key: true,
value: false,
});
- setValidationError('Variable name cannot contain spaces');
+ setValidationError(intl.formatMessage(i18n.noSpaces));
return;
}
@@ -95,11 +128,10 @@ export default function EnvVarsSection({
- Environment Variables
+ {intl.formatMessage(i18n.environmentVariables)}
- Add key-value pairs for environment variables. Click the "+" button to add after filling
- both fields. For existing secret values, click the edit button to modify.
+ {intl.formatMessage(i18n.envVarsDescription)}
@@ -110,7 +142,7 @@ export default function EnvVarsSection({
onChange(index, 'key', e.target.value)}
- placeholder="Variable name"
+ placeholder={intl.formatMessage(i18n.variableName)}
className={cn(
'w-full text-text-primary border-border-primary hover:border-border-primary',
isFieldInvalid(index, 'key') && 'border-red-500 focus:border-red-500'
@@ -127,7 +159,7 @@ export default function EnvVarsSection({
envVar.value === '••••••••' && !envVar.isEdited ? '' : e.target.value;
onChange(index, 'value', newValue);
}}
- placeholder="Value"
+ placeholder={intl.formatMessage(i18n.value)}
className={cn(
'w-full border-border-primary',
envVar.value === '••••••••' && !envVar.isEdited
@@ -166,7 +198,7 @@ export default function EnvVarsSection({
setNewKey(e.target.value);
clearValidation();
}}
- placeholder="Variable name"
+ placeholder={intl.formatMessage(i18n.variableName)}
className={cn(
'w-full text-text-primary border-border-primary hover:border-border-primary',
invalidFields.key && 'border-red-500 focus:border-red-500'
@@ -178,7 +210,7 @@ export default function EnvVarsSection({
setNewValue(e.target.value);
clearValidation();
}}
- placeholder="Value"
+ placeholder={intl.formatMessage(i18n.value)}
className={cn(
'w-full text-text-primary border-border-primary hover:border-border-primary',
invalidFields.value && 'border-red-500 focus:border-red-500'
@@ -190,7 +222,7 @@ export default function EnvVarsSection({
variant="ghost"
className="flex items-center justify-start gap-1 px-2 pr-4 text-sm rounded-full text-text-primary bg-background-primary border border-border-primary hover:border-border-primary transition-colors min-w-[60px] h-9 [&>svg]:!size-4"
>
-
Add
+
{intl.formatMessage(i18n.add)}
diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionConfigFields.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionConfigFields.tsx
index 587c5943..c85e2931 100644
--- a/ui/desktop/src/components/settings/extensions/modal/ExtensionConfigFields.tsx
+++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionConfigFields.tsx
@@ -1,4 +1,32 @@
import { Input } from '../../../ui/input';
+import { defineMessages, useIntl } from '../../../../i18n';
+
+const i18n = defineMessages({
+ commandLabel: {
+ id: 'extensionConfigFields.commandLabel',
+ defaultMessage: 'Command',
+ },
+ commandPlaceholder: {
+ id: 'extensionConfigFields.commandPlaceholder',
+ defaultMessage: 'e.g. npx -y @modelcontextprotocol/my-extension [filepath]',
+ },
+ commandRequired: {
+ id: 'extensionConfigFields.commandRequired',
+ defaultMessage: 'Command is required',
+ },
+ endpointLabel: {
+ id: 'extensionConfigFields.endpointLabel',
+ defaultMessage: 'Endpoint',
+ },
+ endpointPlaceholder: {
+ id: 'extensionConfigFields.endpointPlaceholder',
+ defaultMessage: 'Enter endpoint URL...',
+ },
+ endpointRequired: {
+ id: 'extensionConfigFields.endpointRequired',
+ defaultMessage: 'Endpoint URL is required',
+ },
+});
interface ExtensionConfigFieldsProps {
type: 'stdio' | 'sse' | 'streamable_http' | 'builtin';
@@ -17,20 +45,22 @@ export default function ExtensionConfigFields({
submitAttempted = false,
isValid,
}: ExtensionConfigFieldsProps) {
+ const intl = useIntl();
+
if (type === 'stdio') {
return (
-
Command
+
{intl.formatMessage(i18n.commandLabel)}
onChange('cmd', e.target.value)}
- placeholder="e.g. npx -y @modelcontextprotocol/my-extension
"
+ placeholder={intl.formatMessage(i18n.commandPlaceholder)}
className={`w-full ${!submitAttempted || isValid ? 'border-border-primary' : 'border-red-500'} text-text-primary`}
/>
{submitAttempted && !isValid && (
- Command is required
+ {intl.formatMessage(i18n.commandRequired)}
)}
@@ -39,16 +69,16 @@ export default function ExtensionConfigFields({
} else {
return (
-
Endpoint
+
{intl.formatMessage(i18n.endpointLabel)}
onChange('endpoint', e.target.value)}
- placeholder="Enter endpoint URL..."
+ placeholder={intl.formatMessage(i18n.endpointPlaceholder)}
className={`w-full ${!submitAttempted || isValid ? 'border-border-primary' : 'border-red-500'} text-text-primary`}
/>
{submitAttempted && !isValid && (
-
Endpoint URL is required
+
{intl.formatMessage(i18n.endpointRequired)}
)}
diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionInfoFields.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionInfoFields.tsx
index d4e9ccb6..ba178968 100644
--- a/ui/desktop/src/components/settings/extensions/modal/ExtensionInfoFields.tsx
+++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionInfoFields.tsx
@@ -1,5 +1,53 @@
import { Input } from '../../../ui/input';
import { Select } from '../../../ui/Select';
+import { defineMessages, useIntl } from '../../../../i18n';
+
+const i18n = defineMessages({
+ extensionName: {
+ id: 'extensionInfoFields.extensionName',
+ defaultMessage: 'Extension Name',
+ },
+ extensionNamePlaceholder: {
+ id: 'extensionInfoFields.extensionNamePlaceholder',
+ defaultMessage: 'Enter extension name...',
+ },
+ nameRequired: {
+ id: 'extensionInfoFields.nameRequired',
+ defaultMessage: 'Name is required',
+ },
+ typeLabel: {
+ id: 'extensionInfoFields.typeLabel',
+ defaultMessage: 'Type',
+ },
+ typeStdio: {
+ id: 'extensionInfoFields.typeStdio',
+ defaultMessage: 'STDIO',
+ },
+ typeHttp: {
+ id: 'extensionInfoFields.typeHttp',
+ defaultMessage: 'HTTP',
+ },
+ typeSseUnsupported: {
+ id: 'extensionInfoFields.typeSseUnsupported',
+ defaultMessage: 'SSE (unsupported)',
+ },
+ typeStandardIo: {
+ id: 'extensionInfoFields.typeStandardIo',
+ defaultMessage: 'Standard IO (STDIO)',
+ },
+ typeStreamableHttp: {
+ id: 'extensionInfoFields.typeStreamableHttp',
+ defaultMessage: 'Streamable HTTP',
+ },
+ descriptionLabel: {
+ id: 'extensionInfoFields.descriptionLabel',
+ defaultMessage: 'Description',
+ },
+ descriptionPlaceholder: {
+ id: 'extensionInfoFields.descriptionPlaceholder',
+ defaultMessage: 'Optional description...',
+ },
+});
interface ExtensionInfoFieldsProps {
name: string;
@@ -16,6 +64,8 @@ export default function ExtensionInfoFields({
onChange,
submitAttempted,
}: ExtensionInfoFieldsProps) {
+ const intl = useIntl();
+
const isNameValid = () => {
return name.trim() !== '';
};
@@ -25,33 +75,33 @@ export default function ExtensionInfoFields({
{/* Top row with Name and Type side by side */}
-
Extension Name
+
{intl.formatMessage(i18n.extensionName)}
onChange('name', e.target.value)}
- placeholder="Enter extension name..."
+ placeholder={intl.formatMessage(i18n.extensionNamePlaceholder)}
className={`${!submitAttempted || isNameValid() ? 'border-border-primary' : 'border-red-500'} text-text-primary focus:border-border-primary`}
/>
{submitAttempted && !isNameValid() && (
-
Name is required
+
{intl.formatMessage(i18n.nameRequired)}
)}
{/* Type Dropdown */}
-
Type
+
{intl.formatMessage(i18n.typeLabel)}
{
@@ -61,8 +111,8 @@ export default function ExtensionInfoFields({
}
}}
options={[
- { value: 'stdio', label: 'Standard IO (STDIO)' },
- { value: 'streamable_http', label: 'Streamable HTTP' },
+ { value: 'stdio', label: intl.formatMessage(i18n.typeStandardIo) },
+ { value: 'streamable_http', label: intl.formatMessage(i18n.typeStreamableHttp) },
]}
isSearchable={false}
/>
@@ -71,12 +121,12 @@ export default function ExtensionInfoFields({
{/* Bottom row with Description spanning full width */}
-
Description
+
{intl.formatMessage(i18n.descriptionLabel)}
onChange('description', e.target.value)}
- placeholder="Optional description..."
+ placeholder={intl.formatMessage(i18n.descriptionPlaceholder)}
className={`text-text-primary focus:border-border-primary`}
/>
diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx
index 55abed08..1ae07757 100644
--- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx
+++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx
@@ -1,8 +1,12 @@
import { describe, it, expect, vi } from 'vitest';
-import { render, screen, waitFor } from '@testing-library/react';
+import { render, type RenderOptions, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ExtensionModal from './ExtensionModal';
import { ExtensionFormData } from '../utils';
+import { IntlTestWrapper } from '../../../../i18n/test-utils';
+
+const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) =>
+ render(ui, { wrapper: IntlTestWrapper, ...options });
describe('ExtensionModal', () => {
it('does not show unsaved changes dialog when closing without modifications', async () => {
@@ -25,7 +29,7 @@ describe('ExtensionModal', () => {
headers: [],
};
- render(
+ renderWithIntl(
{
headers: [],
};
- render(
+ renderWithIntl(
{
headers: [],
};
- render(
+ renderWithIntl(
{
headers: [],
};
- render(
+ renderWithIntl(
{
headers: [],
};
- render(
+ renderWithIntl(
(initialData);
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
const [submitAttempted, setSubmitAttempted] = useState(false);
@@ -315,7 +356,7 @@ export default function ExtensionModal({
};
// Update title based on current state
- const modalTitle = showDeleteConfirmation ? `Delete Extension "${formData.name}"` : title;
+ const modalTitle = showDeleteConfirmation ? intl.formatMessage(i18n.deleteExtensionTitle, { name: formData.name }) : title;
return (
<>
@@ -328,7 +369,7 @@ export default function ExtensionModal({
{showDeleteConfirmation && (
- This will permanently remove this extension and all of its settings.
+ {intl.formatMessage(i18n.deleteDescription)}
)}
@@ -336,7 +377,7 @@ export default function ExtensionModal({
{showDeleteConfirmation ? (
- This will permanently remove this extension and all of its settings.
+ {intl.formatMessage(i18n.deleteDescription)}
) : (
@@ -347,7 +388,7 @@ export default function ExtensionModal({
- Installation Notes
+ {intl.formatMessage(i18n.installationNotes)}
{formData.installation_notes}
@@ -426,7 +467,7 @@ export default function ExtensionModal({
{showDeleteConfirmation ? (
<>
setShowDeleteConfirmation(false)}>
- Cancel
+ {intl.formatMessage(i18n.cancel)}
{
@@ -438,7 +479,7 @@ export default function ExtensionModal({
variant="destructive"
>
- Confirm removal
+ {intl.formatMessage(i18n.confirmRemoval)}
>
) : (
@@ -450,11 +491,11 @@ export default function ExtensionModal({
className="text-red-500 hover:text-red-600"
>
- Remove extension
+ {intl.formatMessage(i18n.removeExtension)}
)}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionTimeoutField.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionTimeoutField.tsx
index 8050b21b..bb83af46 100644
--- a/ui/desktop/src/components/settings/extensions/modal/ExtensionTimeoutField.tsx
+++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionTimeoutField.tsx
@@ -1,4 +1,12 @@
import { Input } from '../../../ui/input';
+import { defineMessages, useIntl } from '../../../../i18n';
+
+const i18n = defineMessages({
+ timeoutLabel: {
+ id: 'extensionTimeoutField.timeoutLabel',
+ defaultMessage: 'Timeout',
+ },
+});
interface ExtensionTimeoutFieldProps {
timeout: number;
@@ -24,12 +32,14 @@ export default function ExtensionTimeoutField({
return !isNaN(timeoutValue) && timeoutValue > 0;
};
+ const intl = useIntl();
+
return (
{/* Row with Timeout and timeout input side by side */}
- Timeout
+ {intl.formatMessage(i18n.timeoutLabel)}
(null);
@@ -52,7 +89,7 @@ export default function HeadersSection({
key: keyEmpty,
value: valueEmpty,
});
- setValidationError('Both header name and value must be entered');
+ setValidationError(intl.formatMessage(i18n.bothRequired));
return;
}
@@ -61,7 +98,7 @@ export default function HeadersSection({
key: true,
value: false,
});
- setValidationError('Header name cannot contain spaces');
+ setValidationError(intl.formatMessage(i18n.noSpaces));
return;
}
@@ -70,7 +107,7 @@ export default function HeadersSection({
key: true,
value: false,
});
- setValidationError('A header with this name already exists');
+ setValidationError(intl.formatMessage(i18n.duplicateHeader));
return;
}
@@ -95,10 +132,9 @@ export default function HeadersSection({
return (
-
Request Headers
+
{intl.formatMessage(i18n.requestHeaders)}
- Add custom HTTP headers to include in requests to the MCP server. Click the "+" button to
- add after filling both fields.
+ {intl.formatMessage(i18n.headersDescription)}
@@ -109,7 +145,7 @@ export default function HeadersSection({
onChange(index, 'key', e.target.value)}
- placeholder="Header name"
+ placeholder={intl.formatMessage(i18n.headerName)}
className={cn(
'w-full text-text-primary border-border-primary hover:border-border-primary',
isFieldInvalid(index, 'key') && 'border-red-500 focus:border-red-500'
@@ -120,7 +156,7 @@ export default function HeadersSection({
onChange(index, 'value', e.target.value)}
- placeholder="Value"
+ placeholder={intl.formatMessage(i18n.value)}
className={cn(
'w-full text-text-primary border-border-primary hover:border-border-primary',
isFieldInvalid(index, 'value') && 'border-red-500 focus:border-red-500'
@@ -144,7 +180,7 @@ export default function HeadersSection({
setNewKey(e.target.value);
clearValidation();
}}
- placeholder="Header name"
+ placeholder={intl.formatMessage(i18n.headerName)}
className={cn(
'w-full text-text-primary border-border-primary hover:border-border-primary',
invalidFields.key && 'border-red-500 focus:border-red-500'
@@ -156,7 +192,7 @@ export default function HeadersSection({
setNewValue(e.target.value);
clearValidation();
}}
- placeholder="Value"
+ placeholder={intl.formatMessage(i18n.value)}
className={cn(
'w-full text-text-primary border-border-primary hover:border-border-primary',
invalidFields.value && 'border-red-500 focus:border-red-500'
@@ -167,7 +203,7 @@ export default function HeadersSection({
variant="ghost"
className="flex items-center justify-start gap-1 px-2 pr-4 text-sm rounded-full text-text-primary bg-background-primary border border-border-primary hover:border-border-primary transition-colors min-w-[60px] h-9 [&>svg]:!size-4"
>
-
Add
+
{intl.formatMessage(i18n.add)}
{validationError &&
{validationError}
}
diff --git a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx
index b124eee4..8fb47051 100644
--- a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx
+++ b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx
@@ -5,6 +5,18 @@ import { Gear } from '../../../icons';
import { FixedExtensionEntry } from '../../../ConfigContext';
import { getSubtitle, getFriendlyTitle } from './ExtensionList';
import { Card, CardHeader, CardTitle, CardContent, CardAction } from '../../../ui/card';
+import { defineMessages, useIntl } from '../../../../i18n';
+
+const i18n = defineMessages({
+ configureExtension: {
+ id: 'extensionItem.configureExtension',
+ defaultMessage: 'Configure {name} Extension',
+ },
+ toggleExtension: {
+ id: 'extensionItem.toggleExtension',
+ defaultMessage: 'Toggle {name} extension On or Off',
+ },
+});
interface ExtensionItemProps {
extension: FixedExtensionEntry;
@@ -19,6 +31,7 @@ export default function ExtensionItem({
onConfigure,
isStatic,
}: ExtensionItemProps) {
+ const intl = useIntl();
// Add local state to track the visual toggle state
const [visuallyEnabled, setVisuallyEnabled] = useState(extension.enabled);
// Track if we're in the process of toggling
@@ -84,7 +97,7 @@ export default function ExtensionItem({
{editable && (
onConfigure?.(extension)}
>
@@ -95,7 +108,7 @@ export default function ExtensionItem({
onCheckedChange={() => handleToggle(extension)}
disabled={isToggling}
variant="mono"
- aria-label={`Toggle ${getFriendlyTitle(extension)} extension On or Off`}
+ aria-label={intl.formatMessage(i18n.toggleExtension, { name: getFriendlyTitle(extension) })}
/>
diff --git a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionList.tsx b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionList.tsx
index 087748c8..7b3bbd77 100644
--- a/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionList.tsx
+++ b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionList.tsx
@@ -3,6 +3,26 @@ import builtInExtensionsData from '../../../../built-in-extensions.json';
import { ExtensionConfig } from '../../../../api';
import { FixedExtensionEntry } from '../../../ConfigContext';
import { combineCmdAndArgs } from '../utils';
+import { defineMessages, useIntl } from '../../../../i18n';
+
+const i18n = defineMessages({
+ defaultExtensions: {
+ id: 'extensionList.defaultExtensions',
+ defaultMessage: 'Default Extensions ({count})',
+ },
+ availableExtensions: {
+ id: 'extensionList.availableExtensions',
+ defaultMessage: 'Available Extensions ({count})',
+ },
+ noExtensions: {
+ id: 'extensionList.noExtensions',
+ defaultMessage: 'No extensions available',
+ },
+ builtInExtension: {
+ id: 'extensionList.builtInExtension',
+ defaultMessage: 'Built-in extension',
+ },
+});
interface ExtensionListProps {
extensions: FixedExtensionEntry[];
@@ -35,6 +55,8 @@ export default function ExtensionList({
);
};
+ const intl = useIntl();
+
// Separate enabled and disabled extensions, then filter by search term
const enabledExtensions = extensions.filter((ext) => ext.enabled && matchesSearch(ext));
const disabledExtensions = extensions.filter((ext) => !ext.enabled && matchesSearch(ext));
@@ -53,7 +75,7 @@ export default function ExtensionList({
- Default Extensions ({sortedEnabledExtensions.length})
+ {intl.formatMessage(i18n.defaultExtensions, { count: sortedEnabledExtensions.length })}
{sortedEnabledExtensions.map((extension) => (
@@ -73,7 +95,7 @@ export default function ExtensionList({
- Available Extensions ({sortedDisabledExtensions.length})
+ {intl.formatMessage(i18n.availableExtensions, { count: sortedDisabledExtensions.length })}
{sortedDisabledExtensions.map((extension) => (
@@ -90,7 +112,7 @@ export default function ExtensionList({
)}
{extensions.length === 0 && (
-
No extensions available
+
{intl.formatMessage(i18n.noExtensions)}
)}
);
diff --git a/ui/desktop/src/components/settings/gateways/GatewaySettingsSection.tsx b/ui/desktop/src/components/settings/gateways/GatewaySettingsSection.tsx
index ff944354..dd13b0d6 100644
--- a/ui/desktop/src/components/settings/gateways/GatewaySettingsSection.tsx
+++ b/ui/desktop/src/components/settings/gateways/GatewaySettingsSection.tsx
@@ -3,8 +3,93 @@ import { Button } from '../../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
import { Input } from '../../ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../../ui/dialog';
-import { Loader2, Copy, Check, Square, Trash2, ExternalLink, User } from 'lucide-react';
+import { Loader2, Copy, Check, Square, Trash2, User } from 'lucide-react';
import { getApiUrl } from '../../../config';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ loading: {
+ id: 'gatewaySettings.loading',
+ defaultMessage: 'Loading...',
+ },
+ pairedUsers: {
+ id: 'gatewaySettings.pairedUsers',
+ defaultMessage: 'Paired Users',
+ },
+ telegram: {
+ id: 'gatewaySettings.telegram',
+ defaultMessage: 'Telegram',
+ },
+ running: {
+ id: 'gatewaySettings.running',
+ defaultMessage: 'Running',
+ },
+ stopped: {
+ id: 'gatewaySettings.stopped',
+ defaultMessage: 'Stopped',
+ },
+ pairDevice: {
+ id: 'gatewaySettings.pairDevice',
+ defaultMessage: 'Pair Device',
+ },
+ stop: {
+ id: 'gatewaySettings.stop',
+ defaultMessage: 'Stop',
+ },
+ start: {
+ id: 'gatewaySettings.start',
+ defaultMessage: 'Start',
+ },
+ remove: {
+ id: 'gatewaySettings.remove',
+ defaultMessage: 'Remove',
+ },
+ pasteBotToken: {
+ id: 'gatewaySettings.pasteBotToken',
+ defaultMessage: 'Paste bot token here',
+ },
+ botFatherInstructions: {
+ id: 'gatewaySettings.botFatherInstructions',
+ defaultMessage:
+ 'Open @BotFather on your phone, send /newbot, and follow the prompts to name your bot. BotFather will reply with an API token — paste it below.',
+ },
+ pairingCode: {
+ id: 'gatewaySettings.pairingCode',
+ defaultMessage: 'Pairing Code',
+ },
+ sendCodeToPair: {
+ id: 'gatewaySettings.sendCodeToPair',
+ defaultMessage: 'Send this code to your {gatewayType} bot to pair.',
+ },
+ expiresIn: {
+ id: 'gatewaySettings.expiresIn',
+ defaultMessage: 'Expires in {time}',
+ },
+ close: {
+ id: 'gatewaySettings.close',
+ defaultMessage: 'Close',
+ },
+ failedToStart: {
+ id: 'gatewaySettings.failedToStart',
+ defaultMessage: 'Failed to start',
+ },
+ failedToStop: {
+ id: 'gatewaySettings.failedToStop',
+ defaultMessage: 'Failed to stop',
+ },
+ failedToRemove: {
+ id: 'gatewaySettings.failedToRemove',
+ defaultMessage: 'Failed to remove',
+ },
+ failedToUnpairUser: {
+ id: 'gatewaySettings.failedToUnpairUser',
+ defaultMessage: 'Failed to unpair user',
+ },
+ failedToGeneratePairingCode: {
+ id: 'gatewaySettings.failedToGeneratePairingCode',
+ defaultMessage: 'Failed to generate pairing code',
+ },
+});
interface PairedUserInfo {
platform: string;
@@ -41,6 +126,7 @@ async function gatewayFetch(endpoint: string, options: globalThis.RequestInit =
}
export default function GatewaySettingsSection() {
+ const intl = useIntl();
const [gateways, setGateways] = useState
([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
@@ -92,11 +178,11 @@ export default function GatewaySettingsSection() {
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
- throw new Error(data.message || 'Failed to unpair user');
+ throw new Error(data.message || intl.formatMessage(i18n.failedToUnpairUser));
}
await fetchStatus();
} catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to unpair user');
+ setError(err instanceof Error ? err.message : intl.formatMessage(i18n.failedToUnpairUser));
}
};
@@ -114,7 +200,7 @@ export default function GatewaySettingsSection() {
return (
- Loading...
+ {intl.formatMessage(i18n.loading)}
);
}
@@ -135,14 +221,14 @@ export default function GatewaySettingsSection() {
doPost(
'/gateway/start',
{ gateway_type: 'telegram', platform_config: config, max_sessions: 0 },
- 'Failed to start'
+ intl.formatMessage(i18n.failedToStart)
)
}
onRestart={() =>
- doPost('/gateway/restart', { gateway_type: 'telegram' }, 'Failed to start')
+ doPost('/gateway/restart', { gateway_type: 'telegram' }, intl.formatMessage(i18n.failedToStart))
}
- onStop={() => doPost('/gateway/stop', { gateway_type: 'telegram' }, 'Failed to stop')}
- onRemove={() => doPost('/gateway/remove', { gateway_type: 'telegram' }, 'Failed to remove')}
+ onStop={() => doPost('/gateway/stop', { gateway_type: 'telegram' }, intl.formatMessage(i18n.failedToStop))}
+ onRemove={() => doPost('/gateway/remove', { gateway_type: 'telegram' }, intl.formatMessage(i18n.failedToRemove))}
onGenerateCode={async () => {
setError(null);
try {
@@ -152,13 +238,13 @@ export default function GatewaySettingsSection() {
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
- throw new Error(data.message || 'Failed to generate pairing code');
+ throw new Error(data.message || intl.formatMessage(i18n.failedToGeneratePairingCode));
}
const data: PairingCodeResponse = await response.json();
setPairingCode(data);
setPairingGatewayType('telegram');
} catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to generate pairing code');
+ setError(err instanceof Error ? err.message : intl.formatMessage(i18n.failedToGeneratePairingCode));
}
}}
onUnpairUser={handleUnpairUser}
@@ -186,11 +272,12 @@ function PairedUsersList({
users: PairedUserInfo[];
onUnpairUser: (platform: string, userId: string) => void;
}) {
+ const intl = useIntl();
if (users.length === 0) return null;
return (
-
Paired Users
+
{intl.formatMessage(i18n.pairedUsers)}
{users.map((user) => (
void;
onUnpairUser: (platform: string, userId: string) => void;
}) {
+ const intl = useIntl();
const [botToken, setBotToken] = useState('');
const [busy, setBusy] = useState(false);
const running = status?.running ?? false;
@@ -256,15 +344,15 @@ function TelegramGatewayCard({
- Telegram
+ {intl.formatMessage(i18n.telegram)}
{running && (
- Running
+ {intl.formatMessage(i18n.running)}
)}
{!running && configured && (
- Stopped
+ {intl.formatMessage(i18n.stopped)}
)}
@@ -272,18 +360,18 @@ function TelegramGatewayCard({
{running && (
<>
- Pair Device
+ {intl.formatMessage(i18n.pairDevice)}
- Stop
+ {intl.formatMessage(i18n.stop)}
>
)}
{!running && configured && (
<>
- {busy ? : 'Start'}
+ {busy ? : intl.formatMessage(i18n.start)}
- Remove
+ {intl.formatMessage(i18n.remove)}
>
)}
@@ -305,33 +393,20 @@ function TelegramGatewayCard({
<>
- Open{' '}
-
- @BotFather
-
- {' '}
- on your phone, send{' '}
- /newbot, and follow
- the prompts to name your bot. BotFather will reply with an API token — paste it
- below.
+ {intl.formatMessage(i18n.botFatherInstructions)}
setBotToken(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleFirstStart()}
className="text-sm"
/>
- {busy ? : 'Start'}
+ {busy ? : intl.formatMessage(i18n.start)}
>
@@ -357,6 +432,7 @@ function PairingCodeModal({
onCopy: (text: string) => void;
copied: boolean;
}) {
+ const intl = useIntl();
const [timeRemaining, setTimeRemaining] = useState(0);
useEffect(() => {
@@ -384,7 +460,7 @@ function PairingCodeModal({
!isOpen && onClose()}>
- Pairing Code
+ {intl.formatMessage(i18n.pairingCode)}
@@ -405,18 +481,19 @@ function PairingCodeModal({
- Send this code to your {gatewayType} bot
- to pair.
+ {intl.formatMessage(i18n.sendCodeToPair, { gatewayType })}
- Expires in {minutes}:{seconds.toString().padStart(2, '0')}
+ {intl.formatMessage(i18n.expiresIn, {
+ time: `${minutes}:${seconds.toString().padStart(2, '0')}`,
+ })}
- Close
+ {intl.formatMessage(i18n.close)}
diff --git a/ui/desktop/src/components/settings/keyboard/KeyboardShortcutsSection.tsx b/ui/desktop/src/components/settings/keyboard/KeyboardShortcutsSection.tsx
index b60edf3e..3a8307af 100644
--- a/ui/desktop/src/components/settings/keyboard/KeyboardShortcutsSection.tsx
+++ b/ui/desktop/src/components/settings/keyboard/KeyboardShortcutsSection.tsx
@@ -1,83 +1,291 @@
import { useState, useEffect, useCallback } from 'react';
+import { type MessageDescriptor } from 'react-intl';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { Button } from '../../ui/button';
import { Switch } from '../../ui/switch';
import { ShortcutRecorder } from './ShortcutRecorder';
import { KeyboardShortcuts, defaultKeyboardShortcuts } from '../../../utils/settings';
import { trackSettingToggled } from '../../../utils/analytics';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ // Shortcut labels
+ focusWindowLabel: {
+ id: 'keyboardShortcuts.focusWindowLabel',
+ defaultMessage: 'Focus Goose Window',
+ },
+ focusWindowDescription: {
+ id: 'keyboardShortcuts.focusWindowDescription',
+ defaultMessage: 'Bring Goose window to front from anywhere',
+ },
+ quickLauncherLabel: {
+ id: 'keyboardShortcuts.quickLauncherLabel',
+ defaultMessage: 'Quick Launcher',
+ },
+ quickLauncherDescription: {
+ id: 'keyboardShortcuts.quickLauncherDescription',
+ defaultMessage: 'Open the quick launcher overlay',
+ },
+ newChatLabel: {
+ id: 'keyboardShortcuts.newChatLabel',
+ defaultMessage: 'New Chat',
+ },
+ newChatDescription: {
+ id: 'keyboardShortcuts.newChatDescription',
+ defaultMessage: 'Create a new chat in the current window',
+ },
+ newChatWindowLabel: {
+ id: 'keyboardShortcuts.newChatWindowLabel',
+ defaultMessage: 'New Chat Window',
+ },
+ newChatWindowDescription: {
+ id: 'keyboardShortcuts.newChatWindowDescription',
+ defaultMessage: 'Open a new Goose window',
+ },
+ openDirectoryLabel: {
+ id: 'keyboardShortcuts.openDirectoryLabel',
+ defaultMessage: 'Open Directory',
+ },
+ openDirectoryDescription: {
+ id: 'keyboardShortcuts.openDirectoryDescription',
+ defaultMessage: 'Open directory selection dialog',
+ },
+ settingsLabel: {
+ id: 'keyboardShortcuts.settingsLabel',
+ defaultMessage: 'Settings',
+ },
+ settingsDescription: {
+ id: 'keyboardShortcuts.settingsDescription',
+ defaultMessage: 'Open settings panel',
+ },
+ findLabel: {
+ id: 'keyboardShortcuts.findLabel',
+ defaultMessage: 'Find',
+ },
+ findDescription: {
+ id: 'keyboardShortcuts.findDescription',
+ defaultMessage: 'Open search in conversation',
+ },
+ findNextLabel: {
+ id: 'keyboardShortcuts.findNextLabel',
+ defaultMessage: 'Find Next',
+ },
+ findNextDescription: {
+ id: 'keyboardShortcuts.findNextDescription',
+ defaultMessage: 'Jump to next search result',
+ },
+ findPreviousLabel: {
+ id: 'keyboardShortcuts.findPreviousLabel',
+ defaultMessage: 'Find Previous',
+ },
+ findPreviousDescription: {
+ id: 'keyboardShortcuts.findPreviousDescription',
+ defaultMessage: 'Jump to previous search result',
+ },
+ alwaysOnTopLabel: {
+ id: 'keyboardShortcuts.alwaysOnTopLabel',
+ defaultMessage: 'Always on Top',
+ },
+ alwaysOnTopDescription: {
+ id: 'keyboardShortcuts.alwaysOnTopDescription',
+ defaultMessage: 'Toggle window always on top',
+ },
+ toggleNavigationLabel: {
+ id: 'keyboardShortcuts.toggleNavigationLabel',
+ defaultMessage: 'Toggle Navigation',
+ },
+ toggleNavigationDescription: {
+ id: 'keyboardShortcuts.toggleNavigationDescription',
+ defaultMessage: 'Show or hide the navigation menu',
+ },
+
+ // Category labels and descriptions
+ categoryGlobal: {
+ id: 'keyboardShortcuts.categoryGlobal',
+ defaultMessage: 'Global Shortcuts',
+ },
+ categoryGlobalDescription: {
+ id: 'keyboardShortcuts.categoryGlobalDescription',
+ defaultMessage: 'These shortcuts work system-wide, even when Goose is not focused',
+ },
+ categoryApplication: {
+ id: 'keyboardShortcuts.categoryApplication',
+ defaultMessage: 'Application Shortcuts',
+ },
+ categoryApplicationDescription: {
+ id: 'keyboardShortcuts.categoryApplicationDescription',
+ defaultMessage: 'These shortcuts work when Goose is the active application',
+ },
+ categorySearch: {
+ id: 'keyboardShortcuts.categorySearch',
+ defaultMessage: 'Search Shortcuts',
+ },
+ categorySearchDescription: {
+ id: 'keyboardShortcuts.categorySearchDescription',
+ defaultMessage: 'These shortcuts work when searching in a conversation',
+ },
+ categoryWindow: {
+ id: 'keyboardShortcuts.categoryWindow',
+ defaultMessage: 'Window Shortcuts',
+ },
+ categoryWindowDescription: {
+ id: 'keyboardShortcuts.categoryWindowDescription',
+ defaultMessage: 'These shortcuts control window behavior',
+ },
+
+ // UI strings
+ loading: {
+ id: 'keyboardShortcuts.loading',
+ defaultMessage: 'Loading...',
+ },
+ restartRequired: {
+ id: 'keyboardShortcuts.restartRequired',
+ defaultMessage: 'Restart Required',
+ },
+ restartDescription: {
+ id: 'keyboardShortcuts.restartDescription',
+ defaultMessage:
+ 'Changes to application shortcuts (like New Chat, Settings, etc.) require restarting Goose to take effect. Global shortcuts (Focus Window, Quick Launcher) work immediately.',
+ },
+ dismiss: {
+ id: 'keyboardShortcuts.dismiss',
+ defaultMessage: 'Dismiss',
+ },
+ disabled: {
+ id: 'keyboardShortcuts.disabled',
+ defaultMessage: 'Disabled',
+ },
+ change: {
+ id: 'keyboardShortcuts.change',
+ defaultMessage: 'Change',
+ },
+ resetToDefaultsHeading: {
+ id: 'keyboardShortcuts.resetToDefaultsHeading',
+ defaultMessage: 'Reset to Defaults',
+ },
+ resetToDefaultsDescription: {
+ id: 'keyboardShortcuts.resetToDefaultsDescription',
+ defaultMessage: 'Restore all keyboard shortcuts to their original configuration',
+ },
+ resetAllShortcuts: {
+ id: 'keyboardShortcuts.resetAllShortcuts',
+ defaultMessage: 'Reset All Shortcuts',
+ },
+
+ // Dialog strings
+ shortcutConflictTitle: {
+ id: 'keyboardShortcuts.shortcutConflictTitle',
+ defaultMessage: 'Shortcut Conflict',
+ },
+ shortcutConflictToggleMessage: {
+ id: 'keyboardShortcuts.shortcutConflictToggleMessage',
+ defaultMessage:
+ 'The shortcut {shortcut} is already assigned to "{conflictLabel}".',
+ },
+ shortcutConflictToggleDetail: {
+ id: 'keyboardShortcuts.shortcutConflictToggleDetail',
+ defaultMessage:
+ 'Enabling this will remove the shortcut from "{conflictLabel}" and assign it to "{targetLabel}". Do you want to continue?',
+ },
+ shortcutConflictSaveDetail: {
+ id: 'keyboardShortcuts.shortcutConflictSaveDetail',
+ defaultMessage:
+ 'Saving this will remove the shortcut from "{conflictLabel}" and assign it to "{targetLabel}". Do you want to continue?',
+ },
+ reassignShortcut: {
+ id: 'keyboardShortcuts.reassignShortcut',
+ defaultMessage: 'Reassign Shortcut',
+ },
+ cancel: {
+ id: 'keyboardShortcuts.cancel',
+ defaultMessage: 'Cancel',
+ },
+ resetShortcutsTitle: {
+ id: 'keyboardShortcuts.resetShortcutsTitle',
+ defaultMessage: 'Reset Keyboard Shortcuts',
+ },
+ resetShortcutsMessage: {
+ id: 'keyboardShortcuts.resetShortcutsMessage',
+ defaultMessage: 'Reset all keyboard shortcuts to their default values?',
+ },
+ resetShortcutsDetail: {
+ id: 'keyboardShortcuts.resetShortcutsDetail',
+ defaultMessage: 'This will restore all shortcuts to their original configuration.',
+ },
+});
interface ShortcutConfig {
key: keyof KeyboardShortcuts;
- label: string;
- description: string;
+ label: MessageDescriptor;
+ description: MessageDescriptor;
category: 'global' | 'application' | 'search' | 'window';
}
const shortcutConfigs: ShortcutConfig[] = [
{
key: 'focusWindow',
- label: 'Focus Goose Window',
- description: 'Bring Goose window to front from anywhere',
+ label: i18n.focusWindowLabel,
+ description: i18n.focusWindowDescription,
category: 'global',
},
{
key: 'quickLauncher',
- label: 'Quick Launcher',
- description: 'Open the quick launcher overlay',
+ label: i18n.quickLauncherLabel,
+ description: i18n.quickLauncherDescription,
category: 'global',
},
{
key: 'newChat',
- label: 'New Chat',
- description: 'Create a new chat in the current window',
+ label: i18n.newChatLabel,
+ description: i18n.newChatDescription,
category: 'application',
},
{
key: 'newChatWindow',
- label: 'New Chat Window',
- description: 'Open a new Goose window',
+ label: i18n.newChatWindowLabel,
+ description: i18n.newChatWindowDescription,
category: 'application',
},
{
key: 'openDirectory',
- label: 'Open Directory',
- description: 'Open directory selection dialog',
+ label: i18n.openDirectoryLabel,
+ description: i18n.openDirectoryDescription,
category: 'application',
},
{
key: 'settings',
- label: 'Settings',
- description: 'Open settings panel',
+ label: i18n.settingsLabel,
+ description: i18n.settingsDescription,
category: 'application',
},
{
key: 'find',
- label: 'Find',
- description: 'Open search in conversation',
+ label: i18n.findLabel,
+ description: i18n.findDescription,
category: 'search',
},
{
key: 'findNext',
- label: 'Find Next',
- description: 'Jump to next search result',
+ label: i18n.findNextLabel,
+ description: i18n.findNextDescription,
category: 'search',
},
{
key: 'findPrevious',
- label: 'Find Previous',
- description: 'Jump to previous search result',
+ label: i18n.findPreviousLabel,
+ description: i18n.findPreviousDescription,
category: 'search',
},
{
key: 'alwaysOnTop',
- label: 'Always on Top',
- description: 'Toggle window always on top',
+ label: i18n.alwaysOnTopLabel,
+ description: i18n.alwaysOnTopDescription,
category: 'window',
},
{
key: 'toggleNavigation',
- label: 'Toggle Navigation',
- description: 'Show or hide the navigation menu',
+ label: i18n.toggleNavigationLabel,
+ description: i18n.toggleNavigationDescription,
category: 'application',
},
];
@@ -93,9 +301,12 @@ const needsRestart = new Set([
'alwaysOnTop',
]);
-export const getShortcutLabel = (key: string): string => {
+export const getShortcutLabel = (
+ key: string,
+ formatMessage: (descriptor: MessageDescriptor) => string
+): string => {
const config = shortcutConfigs.find((c) => c.key === key);
- return config?.label || key;
+ return config ? formatMessage(config.label) : key;
};
export const formatShortcut = (shortcut: string): string => {
@@ -108,21 +319,22 @@ export const formatShortcut = (shortcut: string): string => {
.replace('Shift', isMac ? '⇧' : 'Shift');
};
-const categoryLabels = {
- global: 'Global Shortcuts',
- application: 'Application Shortcuts',
- search: 'Search Shortcuts',
- window: 'Window Shortcuts',
+const categoryLabelMessages: Record = {
+ global: i18n.categoryGlobal,
+ application: i18n.categoryApplication,
+ search: i18n.categorySearch,
+ window: i18n.categoryWindow,
};
-const categoryDescriptions = {
- global: 'These shortcuts work system-wide, even when Goose is not focused',
- application: 'These shortcuts work when Goose is the active application',
- search: 'These shortcuts work when searching in a conversation',
- window: 'These shortcuts control window behavior',
+const categoryDescriptionMessages: Record = {
+ global: i18n.categoryGlobalDescription,
+ application: i18n.categoryApplicationDescription,
+ search: i18n.categorySearchDescription,
+ window: i18n.categoryWindowDescription,
};
export default function KeyboardShortcutsSection() {
+ const intl = useIntl();
const [shortcuts, setShortcuts] = useState(null);
const [editingKey, setEditingKey] = useState(null);
const [showRestartNotice, setShowRestartNotice] = useState(false);
@@ -150,10 +362,19 @@ export default function KeyboardShortcutsSection() {
if (conflictingKey) {
const confirmed = await window.electron.showMessageBox({
type: 'warning',
- title: 'Shortcut Conflict',
- message: `The shortcut ${formatShortcut(defaultValue)} is already assigned to "${getShortcutLabel(conflictingKey)}".`,
- detail: `Enabling this will remove the shortcut from "${getShortcutLabel(conflictingKey)}" and assign it to "${getShortcutLabel(key)}". Do you want to continue?`,
- buttons: ['Reassign Shortcut', 'Cancel'],
+ title: intl.formatMessage(i18n.shortcutConflictTitle),
+ message: intl.formatMessage(i18n.shortcutConflictToggleMessage, {
+ shortcut: formatShortcut(defaultValue),
+ conflictLabel: getShortcutLabel(conflictingKey, intl.formatMessage),
+ }),
+ detail: intl.formatMessage(i18n.shortcutConflictToggleDetail, {
+ conflictLabel: getShortcutLabel(conflictingKey, intl.formatMessage),
+ targetLabel: getShortcutLabel(key, intl.formatMessage),
+ }),
+ buttons: [
+ intl.formatMessage(i18n.reassignShortcut),
+ intl.formatMessage(i18n.cancel),
+ ],
defaultId: 1,
});
@@ -191,10 +412,19 @@ export default function KeyboardShortcutsSection() {
if (conflictingKey) {
const confirmed = await window.electron.showMessageBox({
type: 'warning',
- title: 'Shortcut Conflict',
- message: `The shortcut ${formatShortcut(shortcut)} is already assigned to "${getShortcutLabel(conflictingKey)}".`,
- detail: `Saving this will remove the shortcut from "${getShortcutLabel(conflictingKey)}" and assign it to "${getShortcutLabel(editingKey)}". Do you want to continue?`,
- buttons: ['Reassign Shortcut', 'Cancel'],
+ title: intl.formatMessage(i18n.shortcutConflictTitle),
+ message: intl.formatMessage(i18n.shortcutConflictToggleMessage, {
+ shortcut: formatShortcut(shortcut),
+ conflictLabel: getShortcutLabel(conflictingKey, intl.formatMessage),
+ }),
+ detail: intl.formatMessage(i18n.shortcutConflictSaveDetail, {
+ conflictLabel: getShortcutLabel(conflictingKey, intl.formatMessage),
+ targetLabel: getShortcutLabel(editingKey, intl.formatMessage),
+ }),
+ buttons: [
+ intl.formatMessage(i18n.reassignShortcut),
+ intl.formatMessage(i18n.cancel),
+ ],
defaultId: 1,
});
@@ -226,10 +456,13 @@ export default function KeyboardShortcutsSection() {
const handleResetToDefaults = async () => {
const confirmed = await window.electron.showMessageBox({
type: 'question',
- title: 'Reset Keyboard Shortcuts',
- message: 'Reset all keyboard shortcuts to their default values?',
- detail: 'This will restore all shortcuts to their original configuration.',
- buttons: ['Reset to Defaults', 'Cancel'],
+ title: intl.formatMessage(i18n.resetShortcutsTitle),
+ message: intl.formatMessage(i18n.resetShortcutsMessage),
+ detail: intl.formatMessage(i18n.resetShortcutsDetail),
+ buttons: [
+ intl.formatMessage(i18n.resetToDefaultsHeading),
+ intl.formatMessage(i18n.cancel),
+ ],
defaultId: 1,
});
@@ -253,7 +486,7 @@ export default function KeyboardShortcutsSection() {
);
if (!shortcuts) {
- return Loading...
;
+ return {intl.formatMessage(i18n.loading)}
;
}
return (
@@ -263,11 +496,11 @@ export default function KeyboardShortcutsSection() {
-
Restart Required
+
+ {intl.formatMessage(i18n.restartRequired)}
+
- Changes to application shortcuts (like New Chat, Settings, etc.) require
- restarting Goose to take effect. Global shortcuts (Focus Window, Quick Launcher)
- work immediately.
+ {intl.formatMessage(i18n.restartDescription)}
setShowRestartNotice(false)}
className="text-xs shrink-0"
>
- Dismiss
+ {intl.formatMessage(i18n.dismiss)}
@@ -285,9 +518,9 @@ export default function KeyboardShortcutsSection() {
{Object.entries(groupedShortcuts).map(([category, configs]) => (
- {categoryLabels[category as keyof typeof categoryLabels]}
+ {intl.formatMessage(categoryLabelMessages[category])}
- {categoryDescriptions[category as keyof typeof categoryDescriptions]}
+ {intl.formatMessage(categoryDescriptionMessages[category])}
@@ -298,9 +531,11 @@ export default function KeyboardShortcutsSection() {
return (
-
{config.label}
+
+ {intl.formatMessage(config.label)}
+
- {config.description}
+ {intl.formatMessage(config.description)}
@@ -312,7 +547,7 @@ export default function KeyboardShortcutsSection() {
) : (
- Disabled
+ {intl.formatMessage(i18n.disabled)}
)}
handleEdit(config.key)}
className="text-xs"
>
- Change
+ {intl.formatMessage(i18n.change)}
-
Reset to Defaults
+
+ {intl.formatMessage(i18n.resetToDefaultsHeading)}
+
- Restore all keyboard shortcuts to their original configuration
+ {intl.formatMessage(i18n.resetToDefaultsDescription)}
- Reset All Shortcuts
+ {intl.formatMessage(i18n.resetAllShortcuts)}
diff --git a/ui/desktop/src/components/settings/keyboard/ShortcutRecorder.tsx b/ui/desktop/src/components/settings/keyboard/ShortcutRecorder.tsx
index 36c31a4b..96c672f0 100644
--- a/ui/desktop/src/components/settings/keyboard/ShortcutRecorder.tsx
+++ b/ui/desktop/src/components/settings/keyboard/ShortcutRecorder.tsx
@@ -2,6 +2,31 @@ import { useState, useEffect, useRef } from 'react';
import { Button } from '../../ui/button';
import { KeyboardShortcuts } from '../../../utils/settings';
import { getShortcutLabel, formatShortcut } from './KeyboardShortcutsSection';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ pressShortcut: {
+ id: 'shortcutRecorder.pressShortcut',
+ defaultMessage: 'Press shortcut...',
+ },
+ clickToRecord: {
+ id: 'shortcutRecorder.clickToRecord',
+ defaultMessage: 'Click to record...',
+ },
+ save: {
+ id: 'shortcutRecorder.save',
+ defaultMessage: 'Save',
+ },
+ cancel: {
+ id: 'shortcutRecorder.cancel',
+ defaultMessage: 'Cancel',
+ },
+ conflictWarning: {
+ id: 'shortcutRecorder.conflictWarning',
+ defaultMessage:
+ 'This shortcut is already used by {label}. Saving will reassign it to this action.',
+ },
+});
interface ShortcutRecorderProps {
value: string;
@@ -18,6 +43,7 @@ export function ShortcutRecorder({
allShortcuts,
currentKey,
}: ShortcutRecorderProps) {
+ const intl = useIntl();
const [recording, setRecording] = useState(true);
const [capturedShortcut, setCapturedShortcut] = useState(value);
const [displayShortcut, setDisplayShortcut] = useState('');
@@ -151,7 +177,9 @@ export function ShortcutRecorder({
`}
>
{recording ? (
- Press shortcut...
+
+ {intl.formatMessage(i18n.pressShortcut)}
+
) : displayShortcut ? (
{displayShortcut}
@@ -161,7 +189,9 @@ export function ShortcutRecorder({
{formatShortcut(capturedShortcut)}
) : (
- Click to record...
+
+ {intl.formatMessage(i18n.clickToRecord)}
+
)}
- Save
+ {intl.formatMessage(i18n.save)}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
{conflict && (
⚠️
- This shortcut is already used by {getShortcutLabel(conflict)} . Saving
- will reassign it to this action.
+ {intl.formatMessage(i18n.conflictWarning, {
+ label: getShortcutLabel(conflict, intl.formatMessage),
+ })}
)}
diff --git a/ui/desktop/src/components/settings/localInference/HuggingFaceModelSearch.tsx b/ui/desktop/src/components/settings/localInference/HuggingFaceModelSearch.tsx
index 1c49b07e..d152c7d2 100644
--- a/ui/desktop/src/components/settings/localInference/HuggingFaceModelSearch.tsx
+++ b/ui/desktop/src/components/settings/localInference/HuggingFaceModelSearch.tsx
@@ -10,6 +10,62 @@ import {
} from '../../../api';
import { toastError } from '../../../toasts';
import { errorMessage } from '../../../utils/conversionUtils';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ searchHuggingFace: {
+ id: 'huggingFaceModelSearch.searchHuggingFace',
+ defaultMessage: 'Search HuggingFace',
+ },
+ searchPlaceholder: {
+ id: 'huggingFaceModelSearch.searchPlaceholder',
+ defaultMessage: 'Search for GGUF models...',
+ },
+ loadingVariants: {
+ id: 'huggingFaceModelSearch.loadingVariants',
+ defaultMessage: 'Loading variants...',
+ },
+ recommended: {
+ id: 'huggingFaceModelSearch.recommended',
+ defaultMessage: 'Recommended',
+ },
+ download: {
+ id: 'huggingFaceModelSearch.download',
+ defaultMessage: 'Download',
+ },
+ directDownload: {
+ id: 'huggingFaceModelSearch.directDownload',
+ defaultMessage: 'Direct Download',
+ },
+ directDownloadDescription: {
+ id: 'huggingFaceModelSearch.directDownloadDescription',
+ defaultMessage: 'Specify a model directly: {format}',
+ },
+ directDownloadFailed: {
+ id: 'huggingFaceModelSearch.directDownloadFailed',
+ defaultMessage: 'Direct download failed',
+ },
+ directDownloadErrorMsg: {
+ id: 'huggingFaceModelSearch.directDownloadErrorMsg',
+ defaultMessage: 'Failed to start the download. Check the spec: {error}',
+ },
+ noGgufModels: {
+ id: 'huggingFaceModelSearch.noGgufModels',
+ defaultMessage: 'No GGUF models found for this query.',
+ },
+ searchError: {
+ id: 'huggingFaceModelSearch.searchError',
+ defaultMessage: 'Search error: {details}',
+ },
+ searchNoData: {
+ id: 'huggingFaceModelSearch.searchNoData',
+ defaultMessage: 'Search returned no data.',
+ },
+ searchFailed: {
+ id: 'huggingFaceModelSearch.searchFailed',
+ defaultMessage: 'Search failed. Please try again.',
+ },
+});
const formatBytes = (bytes: number): string => {
if (bytes === 0) return 'unknown';
@@ -35,6 +91,7 @@ interface Props {
}
export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
+ const intl = useIntl();
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [expandedRepo, setExpandedRepo] = useState(null);
@@ -93,22 +150,22 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
});
if (validResults.length === 0) {
- setError('No GGUF models found for this query.');
+ setError(intl.formatMessage(i18n.noGgufModels));
}
} else {
console.error('Search response:', response);
const errMsg = response.error
- ? `Search error: ${JSON.stringify(response.error)}`
- : 'Search returned no data.';
+ ? intl.formatMessage(i18n.searchError, { details: JSON.stringify(response.error) })
+ : intl.formatMessage(i18n.searchNoData);
setError(errMsg);
}
} catch (e) {
console.error('Search failed:', e);
- setError('Search failed. Please try again.');
+ setError(intl.formatMessage(i18n.searchFailed));
} finally {
setSearching(false);
}
- }, []);
+ }, [intl]);
const handleQueryChange = (value: string) => {
setQuery(value);
@@ -189,8 +246,8 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
}
} catch (e) {
toastError({
- title: 'Direct download failed',
- msg: 'Failed to start the download. Check the spec: ' + errorMessage(e),
+ title: intl.formatMessage(i18n.directDownloadFailed),
+ msg: intl.formatMessage(i18n.directDownloadErrorMsg, { error: errorMessage(e) }),
});
} finally {
setDownloading((prev) => {
@@ -204,14 +261,14 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
return (
-
Search HuggingFace
+
{intl.formatMessage(i18n.searchHuggingFace)}
handleQueryChange(e.target.value)}
- placeholder="Search for GGUF models..."
+ placeholder={intl.formatMessage(i18n.searchPlaceholder)}
className="w-full pl-9 pr-4 py-2 text-sm border border-border-subtle rounded-lg bg-background-default text-text-default placeholder:text-text-muted focus:outline-none focus:border-accent-primary"
/>
{searching && (
@@ -260,7 +317,7 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
{loadingFiles.has(model.repo_id) && (
- Loading variants...
+ {intl.formatMessage(i18n.loadingVariants)}
)}
{variants.map((variant, idx) => {
@@ -288,7 +345,7 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
{isRecommended && (
- Recommended
+ {intl.formatMessage(i18n.recommended)}
)}
@@ -307,7 +364,7 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
) : (
<>
- Download
+ {intl.formatMessage(i18n.download)}
>
)}
@@ -323,10 +380,11 @@ export const HuggingFaceModelSearch = ({ onDownloadStarted }: Props) => {
)}
-
Direct Download
+
{intl.formatMessage(i18n.directDownload)}
- Specify a model directly:{' '}
- user/repo:quantization
+ {intl.formatMessage(i18n.directDownloadDescription, {
+ format: 'user/repo:quantization',
+ })}
{
) : (
<>
- Download
+ {intl.formatMessage(i18n.download)}
>
)}
diff --git a/ui/desktop/src/components/settings/localInference/LocalInferenceSettings.tsx b/ui/desktop/src/components/settings/localInference/LocalInferenceSettings.tsx
index 76015e6e..e29708cf 100644
--- a/ui/desktop/src/components/settings/localInference/LocalInferenceSettings.tsx
+++ b/ui/desktop/src/components/settings/localInference/LocalInferenceSettings.tsx
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import { Download, Trash2, X, ChevronDown, ChevronUp, Settings2 } from 'lucide-react';
import { Button } from '../../ui/button';
import { useModelAndProvider } from '../../ModelAndProviderContext';
+import { defineMessages, useIntl } from '../../../i18n';
import {
listLocalModels,
downloadHfModel,
@@ -16,6 +17,74 @@ import { HuggingFaceModelSearch } from './HuggingFaceModelSearch';
import { ModelSettingsPanel } from './ModelSettingsPanel';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/dialog';
+const i18n = defineMessages({
+ title: {
+ id: 'localInferenceSettings.title',
+ defaultMessage: 'Local Inference Models',
+ },
+ description: {
+ id: 'localInferenceSettings.description',
+ defaultMessage:
+ 'Download and manage local LLM models for inference without API keys. Search HuggingFace for any GGUF model or use the featured picks below.',
+ },
+ downloading: {
+ id: 'localInferenceSettings.downloading',
+ defaultMessage: 'Downloading',
+ },
+ downloadedModels: {
+ id: 'localInferenceSettings.downloadedModels',
+ defaultMessage: 'Downloaded Models',
+ },
+ featuredModels: {
+ id: 'localInferenceSettings.featuredModels',
+ defaultMessage: 'Featured Models',
+ },
+ recommended: {
+ id: 'localInferenceSettings.recommended',
+ defaultMessage: 'Recommended',
+ },
+ download: {
+ id: 'localInferenceSettings.download',
+ defaultMessage: 'Download',
+ },
+ showRecommendedOnly: {
+ id: 'localInferenceSettings.showRecommendedOnly',
+ defaultMessage: 'Show recommended only',
+ },
+ showAllFeatured: {
+ id: 'localInferenceSettings.showAllFeatured',
+ defaultMessage: 'Show all featured ({count} more)',
+ },
+ modelSettings: {
+ id: 'localInferenceSettings.modelSettings',
+ defaultMessage: 'Model Settings',
+ },
+ noModels: {
+ id: 'localInferenceSettings.noModels',
+ defaultMessage: 'No models available',
+ },
+ downloadProgress: {
+ id: 'localInferenceSettings.downloadProgress',
+ defaultMessage: '{downloaded} / {total} ({percent}%)',
+ },
+ remaining: {
+ id: 'localInferenceSettings.remaining',
+ defaultMessage: '{time} remaining',
+ },
+ downloadFailed: {
+ id: 'localInferenceSettings.downloadFailed',
+ defaultMessage: 'Download failed',
+ },
+ deleteConfirm: {
+ id: 'localInferenceSettings.deleteConfirm',
+ defaultMessage: 'Delete this model? You can re-download it later.',
+ },
+ modelSettingsTitle: {
+ id: 'localInferenceSettings.modelSettingsTitle',
+ defaultMessage: 'Model settings',
+ },
+});
+
const formatBytes = (bytes: number): string => {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`;
@@ -24,6 +93,7 @@ const formatBytes = (bytes: number): string => {
};
export const LocalInferenceSettings = () => {
+ const intl = useIntl();
const [models, setModels] = useState
([]);
const [downloads, setDownloads] = useState>(new Map());
const [showAllFeatured, setShowAllFeatured] = useState(false);
@@ -146,7 +216,7 @@ export const LocalInferenceSettings = () => {
};
const handleDeleteModel = async (modelId: string) => {
- if (!window.confirm('Delete this model? You can re-download it later.')) return;
+ if (!window.confirm(intl.formatMessage(i18n.deleteConfirm))) return;
try {
await deleteLocalModel({ path: { model_id: modelId } });
const updatedModels = await loadModels();
@@ -183,17 +253,16 @@ export const LocalInferenceSettings = () => {
return (
-
Local Inference Models
+
{intl.formatMessage(i18n.title)}
- Download and manage local LLM models for inference without API keys. Search HuggingFace
- for any GGUF model or use the featured picks below.
+ {intl.formatMessage(i18n.description)}
{/* Active Downloads */}
{downloads.size > 0 && (
-
Downloading
+
{intl.formatMessage(i18n.downloading)}
{Array.from(downloads.entries()).map(([modelId, progress]) => {
if (progress.status === 'completed') return null;
@@ -227,17 +296,21 @@ export const LocalInferenceSettings = () => {
- {formatBytes(progress.bytes_downloaded)} /{' '}
- {formatBytes(progress.total_bytes)} (
- {progress.progress_percent.toFixed(0)}%)
+ {intl.formatMessage(i18n.downloadProgress, {
+ downloaded: formatBytes(progress.bytes_downloaded),
+ total: formatBytes(progress.total_bytes),
+ percent: progress.progress_percent.toFixed(0),
+ })}
{progress.eta_seconds != null && progress.eta_seconds > 0 && (
- {progress.eta_seconds < 60
- ? `${Math.round(progress.eta_seconds)}s`
- : `${Math.round(progress.eta_seconds / 60)}m`}{' '}
- remaining
+ {intl.formatMessage(i18n.remaining, {
+ time:
+ progress.eta_seconds < 60
+ ? `${Math.round(progress.eta_seconds)}s`
+ : `${Math.round(progress.eta_seconds / 60)}m`,
+ })}
)}
{progress.speed_bps != null && progress.speed_bps > 0 && (
@@ -249,7 +322,7 @@ export const LocalInferenceSettings = () => {
)}
{progress.status === 'failed' && (
- {progress.error || 'Download failed'}
+ {progress.error || intl.formatMessage(i18n.downloadFailed)}
)}
@@ -262,7 +335,7 @@ export const LocalInferenceSettings = () => {
{/* Downloaded Models */}
{downloadedModels.length > 0 && (
-
Downloaded Models
+
{intl.formatMessage(i18n.downloadedModels)}
{downloadedModels.map((model) => {
const isSelected = selectedModelId === model.id;
@@ -289,7 +362,7 @@ export const LocalInferenceSettings = () => {
{model.recommended && (
- Recommended
+ {intl.formatMessage(i18n.recommended)}
)}
@@ -298,7 +371,7 @@ export const LocalInferenceSettings = () => {
variant="ghost"
size="sm"
onClick={() => setSettingsOpenFor(model.id)}
- title="Model settings"
+ title={intl.formatMessage(i18n.modelSettingsTitle)}
>
@@ -322,7 +395,7 @@ export const LocalInferenceSettings = () => {
{/* Featured Models (not yet downloaded) */}
{displayedFeatured.length > 0 && (
-
Featured Models
+
{intl.formatMessage(i18n.featuredModels)}
{displayedFeatured.map((model) => (
{
{model.recommended && (
- Recommended
+ {intl.formatMessage(i18n.recommended)}
)}
@@ -349,7 +422,7 @@ export const LocalInferenceSettings = () => {
onClick={() => startFeaturedDownload(model.id)}
>
- Download
+ {intl.formatMessage(i18n.download)}
@@ -366,12 +439,14 @@ export const LocalInferenceSettings = () => {
{showAllFeatured ? (
<>
- Show recommended only
+ {intl.formatMessage(i18n.showRecommendedOnly)}
>
) : (
<>
- Show all featured ({notDownloadedModels.length - displayedFeatured.length} more)
+ {intl.formatMessage(i18n.showAllFeatured, {
+ count: notDownloadedModels.length - displayedFeatured.length,
+ })}
>
)}
@@ -385,7 +460,7 @@ export const LocalInferenceSettings = () => {
{models.length === 0 && (
-
No models available
+
{intl.formatMessage(i18n.noModels)}
)}
{
>
- Model Settings
+ {intl.formatMessage(i18n.modelSettings)}
{settingsOpenFor || ''}
{settingsOpenFor && }
diff --git a/ui/desktop/src/components/settings/localInference/ModelSettingsPanel.tsx b/ui/desktop/src/components/settings/localInference/ModelSettingsPanel.tsx
index 523878a1..8887ad65 100644
--- a/ui/desktop/src/components/settings/localInference/ModelSettingsPanel.tsx
+++ b/ui/desktop/src/components/settings/localInference/ModelSettingsPanel.tsx
@@ -8,6 +8,171 @@ import {
type ModelSettings,
type SamplingConfig,
} from '../../../api';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ loadingSettings: {
+ id: 'modelSettingsPanel.loadingSettings',
+ defaultMessage: 'Loading settings...',
+ },
+ saving: {
+ id: 'modelSettingsPanel.saving',
+ defaultMessage: 'Saving...',
+ },
+ reset: {
+ id: 'modelSettingsPanel.reset',
+ defaultMessage: 'Reset',
+ },
+ resetToDefaults: {
+ id: 'modelSettingsPanel.resetToDefaults',
+ defaultMessage: 'Reset to defaults',
+ },
+ contextAndGeneration: {
+ id: 'modelSettingsPanel.contextAndGeneration',
+ defaultMessage: 'Context & Generation',
+ },
+ contextSize: {
+ id: 'modelSettingsPanel.contextSize',
+ defaultMessage: 'Context size',
+ },
+ contextSizeDescription: {
+ id: 'modelSettingsPanel.contextSizeDescription',
+ defaultMessage: 'Max context window (0 = model default)',
+ },
+ maxOutputTokens: {
+ id: 'modelSettingsPanel.maxOutputTokens',
+ defaultMessage: 'Max output tokens',
+ },
+ maxOutputTokensDescription: {
+ id: 'modelSettingsPanel.maxOutputTokensDescription',
+ defaultMessage: 'Cap on generated tokens',
+ },
+ samplingStrategy: {
+ id: 'modelSettingsPanel.samplingStrategy',
+ defaultMessage: 'Sampling Strategy',
+ },
+ temperature: {
+ id: 'modelSettingsPanel.temperature',
+ defaultMessage: 'Temperature',
+ },
+ topK: {
+ id: 'modelSettingsPanel.topK',
+ defaultMessage: 'Top K',
+ },
+ topP: {
+ id: 'modelSettingsPanel.topP',
+ defaultMessage: 'Top P',
+ },
+ minP: {
+ id: 'modelSettingsPanel.minP',
+ defaultMessage: 'Min P',
+ },
+ seed: {
+ id: 'modelSettingsPanel.seed',
+ defaultMessage: 'Seed',
+ },
+ tauTargetEntropy: {
+ id: 'modelSettingsPanel.tauTargetEntropy',
+ defaultMessage: 'Tau (target entropy)',
+ },
+ etaLearningRate: {
+ id: 'modelSettingsPanel.etaLearningRate',
+ defaultMessage: 'Eta (learning rate)',
+ },
+ repetitionPenalty: {
+ id: 'modelSettingsPanel.repetitionPenalty',
+ defaultMessage: 'Repetition Penalty',
+ },
+ repeatPenalty: {
+ id: 'modelSettingsPanel.repeatPenalty',
+ defaultMessage: 'Repeat penalty',
+ },
+ repeatPenaltyDescription: {
+ id: 'modelSettingsPanel.repeatPenaltyDescription',
+ defaultMessage: '1.0 = off',
+ },
+ repeatWindow: {
+ id: 'modelSettingsPanel.repeatWindow',
+ defaultMessage: 'Repeat window',
+ },
+ repeatWindowDescription: {
+ id: 'modelSettingsPanel.repeatWindowDescription',
+ defaultMessage: 'Tokens to look back',
+ },
+ frequencyPenalty: {
+ id: 'modelSettingsPanel.frequencyPenalty',
+ defaultMessage: 'Frequency penalty',
+ },
+ frequencyPenaltyDescription: {
+ id: 'modelSettingsPanel.frequencyPenaltyDescription',
+ defaultMessage: '0.0 = off',
+ },
+ presencePenalty: {
+ id: 'modelSettingsPanel.presencePenalty',
+ defaultMessage: 'Presence penalty',
+ },
+ presencePenaltyDescription: {
+ id: 'modelSettingsPanel.presencePenaltyDescription',
+ defaultMessage: '0.0 = off',
+ },
+ performance: {
+ id: 'modelSettingsPanel.performance',
+ defaultMessage: 'Performance',
+ },
+ batchSize: {
+ id: 'modelSettingsPanel.batchSize',
+ defaultMessage: 'Batch size',
+ },
+ batchSizeDescription: {
+ id: 'modelSettingsPanel.batchSizeDescription',
+ defaultMessage: 'Prompt processing batch',
+ },
+ gpuLayers: {
+ id: 'modelSettingsPanel.gpuLayers',
+ defaultMessage: 'GPU layers',
+ },
+ gpuLayersDescription: {
+ id: 'modelSettingsPanel.gpuLayersDescription',
+ defaultMessage: 'Layers to offload to GPU',
+ },
+ threads: {
+ id: 'modelSettingsPanel.threads',
+ defaultMessage: 'Threads',
+ },
+ threadsDescription: {
+ id: 'modelSettingsPanel.threadsDescription',
+ defaultMessage: 'CPU threads for generation',
+ },
+ lockModelInRam: {
+ id: 'modelSettingsPanel.lockModelInRam',
+ defaultMessage: 'Lock model in RAM (mlock)',
+ },
+ lockModelInRamDescription: {
+ id: 'modelSettingsPanel.lockModelInRamDescription',
+ defaultMessage: 'Prevent model from being swapped to disk',
+ },
+ flashAttention: {
+ id: 'modelSettingsPanel.flashAttention',
+ defaultMessage: 'Flash attention',
+ },
+ flashAttentionDescription: {
+ id: 'modelSettingsPanel.flashAttentionDescription',
+ defaultMessage: 'Enable flash attention optimization',
+ },
+ toolCalling: {
+ id: 'modelSettingsPanel.toolCalling',
+ defaultMessage: 'Tool Calling',
+ },
+ nativeToolCalling: {
+ id: 'modelSettingsPanel.nativeToolCalling',
+ defaultMessage: 'Native tool calling',
+ },
+ nativeToolCallingDescription: {
+ id: 'modelSettingsPanel.nativeToolCallingDescription',
+ defaultMessage:
+ "Use the model's built-in tool-call format instead of the shell-command emulator. Enable for large models that reliably support tool calling.",
+ },
+});
const DEFAULT_SETTINGS: ModelSettings = {
context_size: null,
@@ -138,6 +303,7 @@ function SelectField({
}
export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
+ const intl = useIntl();
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
@@ -201,26 +367,26 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
};
if (loading) {
- return Loading settings...
;
+ return {intl.formatMessage(i18n.loadingSettings)}
;
}
return (
- {saving && Saving... }
-
+ {saving && {intl.formatMessage(i18n.saving)} }
+
- Reset
+ {intl.formatMessage(i18n.reset)}
{/* Context & Generation */}
-
Context & Generation
+
{intl.formatMessage(i18n.contextAndGeneration)}
updateField('context_size', v)}
placeholder="Auto"
@@ -228,8 +394,8 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
allowNull
/>
updateField('max_output_tokens', v)}
placeholder="No limit"
@@ -242,7 +408,7 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
{/* Sampling */}
{
{samplingType === 'Temperature' && settings.sampling?.type === 'Temperature' && (
updateSampling({ temperature: v ?? 0.8 })}
min={0}
@@ -263,13 +429,13 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
step={0.05}
/>
updateSampling({ top_k: v ?? 40 })}
min={0}
/>
updateSampling({ top_p: v ?? 0.95 })}
min={0}
@@ -277,7 +443,7 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
step={0.01}
/>
updateSampling({ min_p: v ?? 0.05 })}
min={0}
@@ -285,7 +451,7 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
step={0.01}
/>
updateSampling({ seed: v })}
placeholder="Random"
@@ -298,14 +464,14 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
{samplingType === 'MirostatV2' && settings.sampling?.type === 'MirostatV2' && (
updateSampling({ tau: v ?? 5.0 })}
min={0}
step={0.1}
/>
updateSampling({ eta: v ?? 0.1 })}
min={0}
@@ -313,7 +479,7 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
step={0.01}
/>
updateSampling({ seed: v })}
placeholder="Random"
@@ -326,26 +492,26 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
{/* Repetition Penalty */}
-
Repetition Penalty
+
{intl.formatMessage(i18n.repetitionPenalty)}
updateField('repeat_penalty', v ?? 1.0)}
min={0}
step={0.05}
/>
updateField('repeat_last_n', v ?? 64)}
min={0}
/>
updateField('frequency_penalty', v ?? 0.0)}
min={0}
@@ -353,8 +519,8 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
step={0.05}
/>
updateField('presence_penalty', v ?? 0.0)}
min={0}
@@ -366,11 +532,11 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
{/* Performance */}
-
Performance
+
{intl.formatMessage(i18n.performance)}
updateField('n_batch', v)}
placeholder="Auto"
@@ -378,8 +544,8 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
allowNull
/>
updateField('n_gpu_layers', v)}
placeholder="All"
@@ -387,8 +553,8 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
allowNull
/>
updateField('n_threads', v)}
placeholder="Auto"
@@ -397,14 +563,14 @@ export const ModelSettingsPanel = ({ modelId }: { modelId: string }) => {
/>
updateField('use_mlock', v)}
/>
{
{/* Tool Calling */}
-
Tool Calling
+
{intl.formatMessage(i18n.toolCalling)}
updateField('native_tool_calling', v)}
/>
diff --git a/ui/desktop/src/components/settings/mode/ConfigureApproveMode.tsx b/ui/desktop/src/components/settings/mode/ConfigureApproveMode.tsx
index 0142e716..d1c4dfca 100644
--- a/ui/desktop/src/components/settings/mode/ConfigureApproveMode.tsx
+++ b/ui/desktop/src/components/settings/mode/ConfigureApproveMode.tsx
@@ -2,6 +2,46 @@ import React, { useEffect, useState } from 'react';
import { Card } from '../../ui/card';
import { Button } from '../../ui/button';
import { GooseMode, ModeSelectionItem } from './ModeSelectionItem';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'configureApproveMode.title',
+ defaultMessage: 'Configure approve mode',
+ },
+ description: {
+ id: 'configureApproveMode.description',
+ defaultMessage: 'Approve requests can either be given to all tool requests or determine which actions may need integration',
+ },
+ manualApproval: {
+ id: 'configureApproveMode.manualApproval',
+ defaultMessage: 'Manual approval',
+ },
+ manualApprovalDescription: {
+ id: 'configureApproveMode.manualApprovalDescription',
+ defaultMessage: 'All tools, extensions and file modifications will require human approval',
+ },
+ smartApproval: {
+ id: 'configureApproveMode.smartApproval',
+ defaultMessage: 'Smart approval',
+ },
+ smartApprovalDescription: {
+ id: 'configureApproveMode.smartApprovalDescription',
+ defaultMessage: 'Intelligently determine which actions need approval based on risk level',
+ },
+ saving: {
+ id: 'configureApproveMode.saving',
+ defaultMessage: 'Saving...',
+ },
+ save: {
+ id: 'configureApproveMode.save',
+ defaultMessage: 'Save',
+ },
+ cancel: {
+ id: 'configureApproveMode.cancel',
+ defaultMessage: 'Cancel',
+ },
+});
interface ConfigureApproveModeProps {
onClose: () => void;
@@ -14,16 +54,17 @@ export function ConfigureApproveMode({
handleModeChange,
currentMode,
}: ConfigureApproveModeProps) {
+ const intl = useIntl();
const approveModes: GooseMode[] = [
{
key: 'approve',
- label: 'Manual approval',
- description: 'All tools, extensions and file modifications will require human approval',
+ labelDescriptor: i18n.manualApproval,
+ descriptionDescriptor: i18n.manualApprovalDescription,
},
{
key: 'smart_approve',
- label: 'Smart approval',
- description: 'Intelligently determine which actions need approval based on risk level ',
+ labelDescriptor: i18n.smartApproval,
+ descriptionDescriptor: i18n.smartApprovalDescription,
},
];
@@ -54,13 +95,12 @@ export function ConfigureApproveMode({
{/* Header */}
-
Configure approve mode
+ {intl.formatMessage(i18n.title)}
- Approve requests can either be given to all tool requests or determine which actions
- may need integration
+ {intl.formatMessage(i18n.description)}
{approveModes.map((mode) => (
@@ -87,7 +127,7 @@ export function ConfigureApproveMode({
onClick={handleModeSubmit}
className="w-full h-[60px] rounded-none border-t border-border-primary hover:bg-background-secondary text-text-primary dark:border-gray-600 text-base font-regular"
>
- {isSubmitting ? 'Saving...' : 'Save'}
+ {isSubmitting ? intl.formatMessage(i18n.saving) : intl.formatMessage(i18n.save)}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
diff --git a/ui/desktop/src/components/settings/mode/ConversationLimitsDropdown.tsx b/ui/desktop/src/components/settings/mode/ConversationLimitsDropdown.tsx
index adbc4b4c..886044c4 100644
--- a/ui/desktop/src/components/settings/mode/ConversationLimitsDropdown.tsx
+++ b/ui/desktop/src/components/settings/mode/ConversationLimitsDropdown.tsx
@@ -1,6 +1,22 @@
import { useState } from 'react';
import { ChevronDown } from 'lucide-react';
import { Input } from '../../ui/input';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ conversationLimits: {
+ id: 'conversationLimitsDropdown.conversationLimits',
+ defaultMessage: 'Conversation Limits',
+ },
+ maxTurns: {
+ id: 'conversationLimitsDropdown.maxTurns',
+ defaultMessage: 'Max Turns',
+ },
+ maxTurnsDescription: {
+ id: 'conversationLimitsDropdown.maxTurnsDescription',
+ defaultMessage: 'Maximum agent turns before Goose asks for user input',
+ },
+});
interface ConversationLimitsDropdownProps {
maxTurns: number;
@@ -11,6 +27,7 @@ export const ConversationLimitsDropdown = ({
maxTurns,
onMaxTurnsChange,
}: ConversationLimitsDropdownProps) => {
+ const intl = useIntl();
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpanded = () => {
@@ -23,7 +40,7 @@ export const ConversationLimitsDropdown = ({
onClick={toggleExpanded}
className="w-full flex items-center justify-between py-2 px-2 hover:bg-background-secondary rounded-lg transition-all group"
>
-
Conversation Limits
+
{intl.formatMessage(i18n.conversationLimits)}
-
Max Turns
+
{intl.formatMessage(i18n.maxTurns)}
- Maximum agent turns before Goose asks for user input
+ {intl.formatMessage(i18n.maxTurnsDescription)}
(
({ currentMode, mode, showDescription, isApproveModeConfigure, handleModeChange }, ref) => {
+ const intl = useIntl();
const [checked, setChecked] = useState(currentMode == mode.key);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isPermissionModalOpen, setIsPermissionModalOpen] = useState(false);
@@ -58,9 +95,9 @@ export const ModeSelectionItem = forwardRef
-
{mode.label}
+
{intl.formatMessage(mode.labelDescriptor)}
{showDescription && (
-
{mode.description}
+
{intl.formatMessage(mode.descriptionDescriptor)}
)}
diff --git a/ui/desktop/src/components/settings/models/ModelsSection.tsx b/ui/desktop/src/components/settings/models/ModelsSection.tsx
index 497f99de..b6aff3fb 100644
--- a/ui/desktop/src/components/settings/models/ModelsSection.tsx
+++ b/ui/desktop/src/components/settings/models/ModelsSection.tsx
@@ -3,20 +3,32 @@ import { View } from '../../../utils/navigationUtils';
import ModelSettingsButtons from './subcomponents/ModelSettingsButtons';
import { useConfig } from '../../ConfigContext';
import {
- UNKNOWN_PROVIDER_MSG,
- UNKNOWN_PROVIDER_TITLE,
+ modelAndProviderMessages,
useModelAndProvider,
} from '../../ModelAndProviderContext';
import { toastError } from '../../../toasts';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import ResetProviderSection from '../reset_provider/ResetProviderSection';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ resetTitle: {
+ id: 'modelsSection.resetTitle',
+ defaultMessage: 'Reset Provider and Model',
+ },
+ resetDescription: {
+ id: 'modelsSection.resetDescription',
+ defaultMessage: 'Clear your selected model and provider settings to start fresh',
+ },
+});
interface ModelsSectionProps {
setView: (view: View) => void;
}
export default function ModelsSection({ setView }: ModelsSectionProps) {
+ const intl = useIntl();
const [provider, setProvider] = useState(null);
const [displayModelName, setDisplayModelName] = useState('');
const [isLoading, setIsLoading] = useState(true);
@@ -48,8 +60,8 @@ export default function ModelsSection({ setView }: ModelsSectionProps) {
if (providerDetailsList.length != 1) {
toastError({
- title: UNKNOWN_PROVIDER_TITLE,
- msg: UNKNOWN_PROVIDER_MSG,
+ title: intl.formatMessage(modelAndProviderMessages.unknownProviderTitle),
+ msg: intl.formatMessage(modelAndProviderMessages.unknownProviderMsg),
});
setProvider(gooseProvider);
} else {
@@ -62,7 +74,7 @@ export default function ModelsSection({ setView }: ModelsSectionProps) {
} finally {
setIsLoading(false);
}
- }, [read, getProviders, getCurrentModelDisplayName, getCurrentProviderDisplayName]);
+ }, [read, getProviders, getCurrentModelDisplayName, getCurrentProviderDisplayName, intl]);
useEffect(() => {
loadModelData();
@@ -104,9 +116,9 @@ export default function ModelsSection({ setView }: ModelsSectionProps) {
- Reset Provider and Model
+ {intl.formatMessage(i18n.resetTitle)}
- Clear your selected model and provider settings to start fresh
+ {intl.formatMessage(i18n.resetDescription)}
diff --git a/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx
index 8e4d956d..f5791823 100644
--- a/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx
+++ b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx
@@ -16,6 +16,30 @@ import { Alert } from '../../../alerts';
import BottomMenuAlertPopover from '../../../bottom_menu/BottomMenuAlertPopover';
import { ModelSettingsPanel } from '../../localInference/ModelSettingsPanel';
import { ScrollArea } from '../../../ui/scroll-area';
+import { defineMessages, useIntl } from '../../../../i18n';
+
+const i18n = defineMessages({
+ selectModel: {
+ id: 'modelsBottomBar.selectModel',
+ defaultMessage: 'Select Model',
+ },
+ currentModel: {
+ id: 'modelsBottomBar.currentModel',
+ defaultMessage: 'Current model',
+ },
+ changeModel: {
+ id: 'modelsBottomBar.changeModel',
+ defaultMessage: 'Change Model',
+ },
+ localModelSettings: {
+ id: 'modelsBottomBar.localModelSettings',
+ defaultMessage: 'Local Model Settings',
+ },
+ localModelSettingsTitle: {
+ id: 'modelsBottomBar.localModelSettingsTitle',
+ defaultMessage: 'Local Model Settings — {modelName}',
+ },
+});
interface ModelsBottomBarProps {
sessionId: string | null;
@@ -44,9 +68,10 @@ export default function ModelsBottomBar({
const currentModel = sessionModel ?? configModel;
const currentProvider = sessionProvider ?? configProvider;
+ const intl = useIntl();
const { getProviders } = useConfig();
const [displayProvider, setDisplayProvider] = useState(null);
- const [displayModelName, setDisplayModelName] = useState('Select Model');
+ const [displayModelName, setDisplayModelName] = useState(intl.formatMessage(i18n.selectModel));
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
const [isLocalModelSettingsOpen, setIsLocalModelSettingsOpen] = useState(false);
const [providerDefaultModel, setProviderDefaultModel] = useState(null);
@@ -106,18 +131,18 @@ export default function ModelsBottomBar({
- Current model
+ {intl.formatMessage(i18n.currentModel)}
{displayModelName}
{displayProvider && ` — ${displayProvider}`}
setIsAddModelModalOpen(true)}>
- Change Model
+ {intl.formatMessage(i18n.changeModel)}
{currentProvider === 'local' && currentModel && (
setIsLocalModelSettingsOpen(true)}>
- Local Model Settings
+ {intl.formatMessage(i18n.localModelSettings)}
)}
@@ -140,7 +165,7 @@ export default function ModelsBottomBar({
- Local Model Settings — {getModelDisplayName(currentModel)}
+ {intl.formatMessage(i18n.localModelSettingsTitle, { modelName: getModelDisplayName(currentModel) })}
setIsLocalModelSettingsOpen(false)}
diff --git a/ui/desktop/src/components/settings/models/subcomponents/ModelSettingsButtons.tsx b/ui/desktop/src/components/settings/models/subcomponents/ModelSettingsButtons.tsx
index bc20bbfe..620a4ef5 100644
--- a/ui/desktop/src/components/settings/models/subcomponents/ModelSettingsButtons.tsx
+++ b/ui/desktop/src/components/settings/models/subcomponents/ModelSettingsButtons.tsx
@@ -3,12 +3,25 @@ import { Button } from '../../../ui/button';
import { SwitchModelModal } from './SwitchModelModal';
import type { View } from '../../../../utils/navigationUtils';
import { shouldShowPredefinedModels } from '../predefinedModelsUtils';
+import { defineMessages, useIntl } from '../../../../i18n';
+
+const i18n = defineMessages({
+ switchModels: {
+ id: 'modelSettingsButtons.switchModels',
+ defaultMessage: 'Switch models',
+ },
+ configureProviders: {
+ id: 'modelSettingsButtons.configureProviders',
+ defaultMessage: 'Configure providers',
+ },
+});
interface ConfigureModelButtonsProps {
setView: (view: View) => void;
}
export default function ModelSettingsButtons({ setView }: ConfigureModelButtonsProps) {
+ const intl = useIntl();
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
const hasPredefinedModels = shouldShowPredefinedModels();
@@ -20,7 +33,7 @@ export default function ModelSettingsButtons({ setView }: ConfigureModelButtonsP
size="sm"
onClick={() => setIsAddModelModalOpen(true)}
>
- Switch models
+ {intl.formatMessage(i18n.switchModels)}
{isAddModelModalOpen ? (
- Configure providers
+ {intl.formatMessage(i18n.configureProviders)}
)}
diff --git a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx
index 279abdbb..275d779b 100644
--- a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx
+++ b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { Bot, ExternalLink } from 'lucide-react';
+import { defineMessages, useIntl } from '../../../../i18n';
import {
Dialog,
@@ -21,17 +22,170 @@ import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../prede
import { ProviderType } from '../../../../api';
import { trackModelChanged } from '../../../../utils/analytics';
-const THINKING_LEVEL_OPTIONS = [
- { value: 'low', label: 'Low - Better latency, lighter reasoning' },
- { value: 'high', label: 'High - Deeper reasoning, higher latency' },
-];
+const i18n = defineMessages({
+ thinkingLevelLow: {
+ id: 'switchModelModal.thinkingLevelLow',
+ defaultMessage: 'Low - Better latency, lighter reasoning',
+ },
+ thinkingLevelHigh: {
+ id: 'switchModelModal.thinkingLevelHigh',
+ defaultMessage: 'High - Deeper reasoning, higher latency',
+ },
+ claudeEffortLow: {
+ id: 'switchModelModal.claudeEffortLow',
+ defaultMessage: 'Low - Minimal thinking, fastest responses',
+ },
+ claudeEffortMedium: {
+ id: 'switchModelModal.claudeEffortMedium',
+ defaultMessage: 'Medium - Moderate thinking',
+ },
+ claudeEffortHigh: {
+ id: 'switchModelModal.claudeEffortHigh',
+ defaultMessage: 'High - Deep reasoning (default)',
+ },
+ claudeEffortMax: {
+ id: 'switchModelModal.claudeEffortMax',
+ defaultMessage: 'Max - No constraints on thinking depth',
+ },
+ selectModel: {
+ id: 'switchModelModal.selectModel',
+ defaultMessage: 'Please select a model',
+ },
+ selectProvider: {
+ id: 'switchModelModal.selectProvider',
+ defaultMessage: 'Please select a provider',
+ },
+ selectOrEnterModel: {
+ id: 'switchModelModal.selectOrEnterModel',
+ defaultMessage: 'Please select or enter a model',
+ },
+ title: {
+ id: 'switchModelModal.title',
+ defaultMessage: 'Switch models',
+ },
+ description: {
+ id: 'switchModelModal.description',
+ defaultMessage: 'Select a provider and model to use for your conversations.',
+ },
+ chooseModel: {
+ id: 'switchModelModal.chooseModel',
+ defaultMessage: 'Choose a model:',
+ },
+ recommended: {
+ id: 'switchModelModal.recommended',
+ defaultMessage: 'Recommended',
+ },
+ thinkingLevel: {
+ id: 'switchModelModal.thinkingLevel',
+ defaultMessage: 'Thinking Level',
+ },
+ geminiOnly: {
+ id: 'switchModelModal.geminiOnly',
+ defaultMessage: '(Gemini 3 models only)',
+ },
+ selectThinkingLevel: {
+ id: 'switchModelModal.selectThinkingLevel',
+ defaultMessage: 'Select thinking level',
+ },
+ useOtherProvider: {
+ id: 'switchModelModal.useOtherProvider',
+ defaultMessage: 'Use other provider',
+ },
+ providerPlaceholder: {
+ id: 'switchModelModal.providerPlaceholder',
+ defaultMessage: 'Provider, type to search',
+ },
+ localModelsTitle: {
+ id: 'switchModelModal.localModelsTitle',
+ defaultMessage: 'Local models need to be downloaded first',
+ },
+ localModelsDescription: {
+ id: 'switchModelModal.localModelsDescription',
+ defaultMessage: 'To use local inference, you need to download a model to your computer first. Go to Settings → Models to manage local models.',
+ },
+ goToSettings: {
+ id: 'switchModelModal.goToSettings',
+ defaultMessage: 'Go to Settings',
+ },
+ couldNotContactProvider: {
+ id: 'switchModelModal.couldNotContactProvider',
+ defaultMessage: 'Could not contact provider',
+ },
+ checkProviderConfig: {
+ id: 'switchModelModal.checkProviderConfig',
+ defaultMessage: 'Check your provider configuration in Settings → Providers',
+ },
+ loadingModels: {
+ id: 'switchModelModal.loadingModels',
+ defaultMessage: 'Loading models…',
+ },
+ selectModelPlaceholder: {
+ id: 'switchModelModal.selectModelPlaceholder',
+ defaultMessage: 'Select a model, type to search',
+ },
+ customModelName: {
+ id: 'switchModelModal.customModelName',
+ defaultMessage: 'Custom model name',
+ },
+ backToModelList: {
+ id: 'switchModelModal.backToModelList',
+ defaultMessage: 'Back to model list',
+ },
+ typeModelName: {
+ id: 'switchModelModal.typeModelName',
+ defaultMessage: 'Type model name here',
+ },
+ extendedThinking: {
+ id: 'switchModelModal.extendedThinking',
+ defaultMessage: 'Extended Thinking',
+ },
+ selectThinkingMode: {
+ id: 'switchModelModal.selectThinkingMode',
+ defaultMessage: 'Select thinking mode',
+ },
+ thinkingEffort: {
+ id: 'switchModelModal.thinkingEffort',
+ defaultMessage: 'Thinking Effort',
+ },
+ selectEffortLevel: {
+ id: 'switchModelModal.selectEffortLevel',
+ defaultMessage: 'Select effort level',
+ },
+ thinkingBudget: {
+ id: 'switchModelModal.thinkingBudget',
+ defaultMessage: 'Thinking Budget (tokens)',
+ },
+ quickStartGuide: {
+ id: 'switchModelModal.quickStartGuide',
+ defaultMessage: 'Quick start guide',
+ },
+ cancel: {
+ id: 'switchModelModal.cancel',
+ defaultMessage: 'Cancel',
+ },
+ selectModelButton: {
+ id: 'switchModelModal.selectModelButton',
+ defaultMessage: 'Select model',
+ },
+ enterModelNotListed: {
+ id: 'switchModelModal.enterModelNotListed',
+ defaultMessage: 'Enter a model not listed...',
+ },
+ claudeAdaptive: {
+ id: 'switchModelModal.claudeAdaptive',
+ defaultMessage: 'Adaptive - Claude decides when and how much to think',
+ },
+ claudeEnabled: {
+ id: 'switchModelModal.claudeEnabled',
+ defaultMessage: 'Enabled - Fixed token budget for thinking',
+ },
+ claudeDisabled: {
+ id: 'switchModelModal.claudeDisabled',
+ defaultMessage: 'Disabled - No extended thinking',
+ },
+});
-const CLAUDE_THINKING_EFFORT_OPTIONS = [
- { value: 'low', label: 'Low - Minimal thinking, fastest responses' },
- { value: 'medium', label: 'Medium - Moderate thinking' },
- { value: 'high', label: 'High - Deep reasoning (default)' },
- { value: 'max', label: 'Max - No constraints on thinking depth' },
-];
+// THINKING_LEVEL_OPTIONS and CLAUDE_THINKING_EFFORT_OPTIONS are created inside the component to support i18n.
function isClaudeModel(name: string | null | undefined): boolean {
return !!name && name.toLowerCase().startsWith('claude-');
@@ -100,6 +254,20 @@ export const SwitchModelModal = ({
sessionModel,
sessionProvider,
}: SwitchModelModalProps) => {
+ const intl = useIntl();
+
+ const THINKING_LEVEL_OPTIONS = [
+ { value: 'low', label: intl.formatMessage(i18n.thinkingLevelLow) },
+ { value: 'high', label: intl.formatMessage(i18n.thinkingLevelHigh) },
+ ];
+
+ const CLAUDE_THINKING_EFFORT_OPTIONS = [
+ { value: 'low', label: intl.formatMessage(i18n.claudeEffortLow) },
+ { value: 'medium', label: intl.formatMessage(i18n.claudeEffortMedium) },
+ { value: 'high', label: intl.formatMessage(i18n.claudeEffortHigh) },
+ { value: 'max', label: intl.formatMessage(i18n.claudeEffortMax) },
+ ];
+
const { getProviders, read, upsert } = useConfig();
const {
changeModel,
@@ -179,17 +347,17 @@ export const SwitchModelModal = ({
if (usePredefinedModels) {
if (!selectedPredefinedModel) {
- errors.model = 'Please select a model';
+ errors.model = intl.formatMessage(i18n.selectModel);
formIsValid = false;
}
} else {
if (!provider) {
- errors.provider = 'Please select a provider';
+ errors.provider = intl.formatMessage(i18n.selectProvider);
formIsValid = false;
}
if (!model) {
- errors.model = 'Please select or enter a model';
+ errors.model = intl.formatMessage(i18n.selectOrEnterModel);
formIsValid = false;
}
}
@@ -197,7 +365,7 @@ export const SwitchModelModal = ({
setValidationErrors(errors);
setIsValid(formIsValid);
return formIsValid;
- }, [model, provider, usePredefinedModels, selectedPredefinedModel]);
+ }, [model, provider, usePredefinedModels, selectedPredefinedModel, intl]);
const handleClose = () => {
onClose();
@@ -316,7 +484,7 @@ export const SwitchModelModal = ({
})),
{
value: 'configure_providers',
- label: 'Use other provider',
+ label: intl.formatMessage(i18n.useOtherProvider),
},
]);
@@ -357,7 +525,7 @@ export const SwitchModelModal = ({
if (p.provider_type !== 'Custom') {
options.push({
value: 'custom',
- label: 'Enter a model not listed...',
+ label: intl.formatMessage(i18n.enterModelNotListed),
provider: p.name,
providerType: p.provider_type,
});
@@ -380,7 +548,7 @@ export const SwitchModelModal = ({
setLoadingModels(false);
}
})();
- }, [getProviders, usePredefinedModels, read]);
+ }, [getProviders, usePredefinedModels, read, intl]);
const filteredModelOptions = provider
? modelOptions.filter((group) => group.options[0]?.provider === provider)
@@ -471,16 +639,16 @@ export const SwitchModelModal = ({
const claudeThinkingTypeOptions = [
...(modelSupportsAdaptive
- ? [{ value: 'adaptive', label: 'Adaptive - Claude decides when and how much to think' }]
+ ? [{ value: 'adaptive', label: intl.formatMessage(i18n.claudeAdaptive) }]
: []),
- { value: 'enabled', label: 'Enabled - Fixed token budget for thinking' },
- { value: 'disabled', label: 'Disabled - No extended thinking' },
+ { value: 'enabled', label: intl.formatMessage(i18n.claudeEnabled) },
+ { value: 'disabled', label: intl.formatMessage(i18n.claudeDisabled) },
];
const claudeThinkingControls = showClaudeThinking && (
- Extended Thinking
+ {intl.formatMessage(i18n.extendedThinking)}
o.value === claudeThinkingType)}
@@ -488,12 +656,12 @@ export const SwitchModelModal = ({
const option = newValue as { value: string; label: string } | null;
setClaudeThinkingType(option?.value || 'disabled');
}}
- placeholder="Select thinking mode"
+ placeholder={intl.formatMessage(i18n.selectThinkingMode)}
/>
{claudeThinkingType === 'adaptive' && (
- Thinking Effort
+ {intl.formatMessage(i18n.thinkingEffort)}
o.value === claudeThinkingEffort)}
@@ -501,13 +669,13 @@ export const SwitchModelModal = ({
const option = newValue as { value: string; label: string } | null;
setClaudeThinkingEffort(option?.value || 'high');
}}
- placeholder="Select effort level"
+ placeholder={intl.formatMessage(i18n.selectEffortLevel)}
/>
)}
{claudeThinkingType === 'enabled' && (
-
Thinking Budget (tokens)
+
{intl.formatMessage(i18n.thinkingBudget)}
- {titleOverride || 'Switch models'}
+ {titleOverride || intl.formatMessage(i18n.title)}
- Select a provider and model to use for your conversations.
+ {intl.formatMessage(i18n.description)}
@@ -537,7 +705,7 @@ export const SwitchModelModal = ({
{usePredefinedModels ? (
- Choose a model:
+ {intl.formatMessage(i18n.chooseModel)}
@@ -558,7 +726,7 @@ export const SwitchModelModal = ({
{model.alias?.includes('recommended') && (
- Recommended
+ {intl.formatMessage(i18n.recommended)}
)}
@@ -597,8 +765,8 @@ export const SwitchModelModal = ({
{isGemini3Model && (
- Thinking Level
- (Gemini 3 models only)
+ {intl.formatMessage(i18n.thinkingLevel)}
+ {intl.formatMessage(i18n.geminiOnly)}
)}
@@ -634,7 +802,7 @@ export const SwitchModelModal = ({
setUserClearedModel(false);
}
}}
- placeholder="Provider, type to search"
+ placeholder={intl.formatMessage(i18n.providerPlaceholder)}
isClearable
/>
{attemptedSubmit && validationErrors.provider && (
@@ -653,11 +821,10 @@ export const SwitchModelModal = ({
- Local models need to be downloaded first
+ {intl.formatMessage(i18n.localModelsTitle)}
- To use local inference, you need to download a model to your computer
- first. Go to Settings → Models to manage local models.
+ {intl.formatMessage(i18n.localModelsDescription)}
- Go to Settings
+ {intl.formatMessage(i18n.goToSettings)}
@@ -679,13 +846,13 @@ export const SwitchModelModal = ({
- Could not contact provider
+ {intl.formatMessage(i18n.couldNotContactProvider)}
{providerErrors[provider]}
- Check your provider configuration in Settings → Providers
+ {intl.formatMessage(i18n.checkProviderConfig)}
@@ -704,12 +871,12 @@ export const SwitchModelModal = ({
onInputChange={handleInputChange}
value={
loadingModels
- ? { value: '', label: 'Loading models…', isDisabled: true }
+ ? { value: '', label: intl.formatMessage(i18n.loadingModels), isDisabled: true }
: model
? { value: model, label: model }
: null
}
- placeholder="Select a model, type to search"
+ placeholder={intl.formatMessage(i18n.selectModelPlaceholder)}
isClearable
isDisabled={loadingModels}
/>
@@ -728,17 +895,17 @@ export const SwitchModelModal = ({
) : (
- Custom model name
+ {intl.formatMessage(i18n.customModelName)}
setIsCustomModel(false)}
className="text-sm text-text-secondary"
>
- Back to model list
+ {intl.formatMessage(i18n.backToModelList)}
setModel(event.target.value)}
value={model}
/>
@@ -781,14 +948,14 @@ export const SwitchModelModal = ({
className="inline-flex items-center text-text-secondary hover:text-text-primary text-sm mr-auto"
>
- Quick start guide
+ {intl.formatMessage(i18n.quickStartGuide)}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
- Select model
+ {intl.formatMessage(i18n.selectModelButton)}
diff --git a/ui/desktop/src/components/settings/permission/PermissionModal.tsx b/ui/desktop/src/components/settings/permission/PermissionModal.tsx
index 4c44a190..78c3aefe 100644
--- a/ui/desktop/src/components/settings/permission/PermissionModal.tsx
+++ b/ui/desktop/src/components/settings/permission/PermissionModal.tsx
@@ -10,6 +10,56 @@ import {
DropdownMenuItem,
} from '../../ui/dropdown-menu';
import { useChatContext } from '../../../contexts/ChatContext';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ alwaysAllow: {
+ id: 'permissionModal.alwaysAllow',
+ defaultMessage: 'Always allow',
+ },
+ askBefore: {
+ id: 'permissionModal.askBefore',
+ defaultMessage: 'Ask before',
+ },
+ neverAllow: {
+ id: 'permissionModal.neverAllow',
+ defaultMessage: 'Never allow',
+ },
+ noActiveSession: {
+ id: 'permissionModal.noActiveSession',
+ defaultMessage: 'No active session',
+ },
+ noActiveSessionDescription: {
+ id: 'permissionModal.noActiveSessionDescription',
+ defaultMessage:
+ 'Start a chat session first to configure tool permissions for this extension. Tool permissions are loaded from the active session\'s extensions.',
+ },
+ failedToLoadTools: {
+ id: 'permissionModal.failedToLoadTools',
+ defaultMessage: 'Failed to load tools',
+ },
+ failedToLoadToolsDescription: {
+ id: 'permissionModal.failedToLoadToolsDescription',
+ defaultMessage:
+ 'Could not load tools for this extension. The extension may not be loaded in the current session.',
+ },
+ noToolsAvailable: {
+ id: 'permissionModal.noToolsAvailable',
+ defaultMessage: 'No tools available for this extension.',
+ },
+ close: {
+ id: 'permissionModal.close',
+ defaultMessage: 'Close',
+ },
+ cancel: {
+ id: 'permissionModal.cancel',
+ defaultMessage: 'Cancel',
+ },
+ saveChanges: {
+ id: 'permissionModal.saveChanges',
+ defaultMessage: 'Save Changes',
+ },
+});
function getFirstSentence(text: string): string {
const match = text.match(/^([^.?!]+[.?!])/);
@@ -22,10 +72,12 @@ interface PermissionModalProps {
}
export default function PermissionModal({ extensionName, onClose }: PermissionModalProps) {
+ const intl = useIntl();
+
const permissionOptions = [
- { value: 'always_allow', label: 'Always allow' },
- { value: 'ask_before', label: 'Ask before' },
- { value: 'never_allow', label: 'Never allow' },
+ { value: 'always_allow', label: intl.formatMessage(i18n.alwaysAllow) },
+ { value: 'ask_before', label: intl.formatMessage(i18n.askBefore) },
+ { value: 'never_allow', label: intl.formatMessage(i18n.neverAllow) },
] as { value: PermissionLevel; label: string }[];
const chatContext = useChatContext();
@@ -157,24 +209,22 @@ export default function PermissionModal({ extensionName, onClose }: PermissionMo
) : loadError === 'no_session' ? (
-
No active session
+
{intl.formatMessage(i18n.noActiveSession)}
- Start a chat session first to configure tool permissions for this extension. Tool
- permissions are loaded from the active session's extensions.
+ {intl.formatMessage(i18n.noActiveSessionDescription)}
) : loadError === 'fetch_failed' ? (
-
Failed to load tools
+
{intl.formatMessage(i18n.failedToLoadTools)}
- Could not load tools for this extension. The extension may not be loaded in the
- current session.
+ {intl.formatMessage(i18n.failedToLoadToolsDescription)}
) : tools.length === 0 ? (
-
No tools available for this extension.
+
{intl.formatMessage(i18n.noToolsAvailable)}
) : (
@@ -197,7 +247,7 @@ export default function PermissionModal({ extensionName, onClose }: PermissionMo
{permissionOptions.find(
(option) =>
option.value === (updatedPermissions[tool.name] || tool.permission)
- )?.label || 'Ask Before'}
+ )?.label || intl.formatMessage(i18n.askBefore)}
@@ -222,11 +272,11 @@ export default function PermissionModal({ extensionName, onClose }: PermissionMo
- {loadError ? 'Close' : 'Cancel'}
+ {loadError ? intl.formatMessage(i18n.close) : intl.formatMessage(i18n.cancel)}
{!loadError && (
- Save Changes
+ {intl.formatMessage(i18n.saveChanges)}
)}
diff --git a/ui/desktop/src/components/settings/permission/PermissionRulesModal.tsx b/ui/desktop/src/components/settings/permission/PermissionRulesModal.tsx
index 2ed4a3a7..f21f858b 100644
--- a/ui/desktop/src/components/settings/permission/PermissionRulesModal.tsx
+++ b/ui/desktop/src/components/settings/permission/PermissionRulesModal.tsx
@@ -4,6 +4,23 @@ import { FixedExtensionEntry, useConfig } from '../../ConfigContext';
import { ChevronRight } from 'lucide-react';
import PermissionModal from './PermissionModal';
import { Button } from '../../ui/button';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'permissionRulesModal.title',
+ defaultMessage: 'Permission Rules',
+ },
+ description: {
+ id: 'permissionRulesModal.description',
+ defaultMessage:
+ 'Configure tool permissions for extensions to control how they interact with your system.',
+ },
+ extensionRules: {
+ id: 'permissionRulesModal.extensionRules',
+ defaultMessage: 'Extension rules',
+ },
+});
function RuleItem({ title, description }: { title: string; description: string }) {
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -46,6 +63,7 @@ interface PermissionRulesModalProps {
}
export default function PermissionRulesModal({ isOpen, onClose }: PermissionRulesModalProps) {
+ const intl = useIntl();
const { getExtensions } = useConfig();
const [extensions, setExtensions] = useState([]);
@@ -107,11 +125,10 @@ export default function PermissionRulesModal({ isOpen, onClose }: PermissionRule
- Permission Rules
+ {intl.formatMessage(i18n.title)}
- Configure tool permissions for extensions to control how they interact with your
- system.
+ {intl.formatMessage(i18n.description)}
@@ -121,7 +138,7 @@ export default function PermissionRulesModal({ isOpen, onClose }: PermissionRule
{/* Extension Rules Section */}
{extensions.map((extension) => (
diff --git a/ui/desktop/src/components/settings/permission/PermissionSetting.tsx b/ui/desktop/src/components/settings/permission/PermissionSetting.tsx
index 74394af1..3add6fc3 100644
--- a/ui/desktop/src/components/settings/permission/PermissionSetting.tsx
+++ b/ui/desktop/src/components/settings/permission/PermissionSetting.tsx
@@ -5,6 +5,23 @@ import { FixedExtensionEntry, useConfig } from '../../ConfigContext';
import { ChevronRight } from 'lucide-react';
import PermissionModal from './PermissionModal';
import { Button } from '../../ui/button';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ permissionRules: {
+ id: 'permissionSetting.permissionRules',
+ defaultMessage: 'Permission Rules',
+ },
+ permissionRulesDescription: {
+ id: 'permissionSetting.permissionRulesDescription',
+ defaultMessage:
+ 'Hidden instructions that will be passed to the provider to help direct and add context to your responses.',
+ },
+ extensionRules: {
+ id: 'permissionSetting.extensionRules',
+ defaultMessage: 'Extension rules',
+ },
+});
function RuleItem({ title, description }: { title: string; description: string }) {
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -43,6 +60,7 @@ function RulesSection({ title, rules }: { title: string; rules: React.ReactNode
}
export default function PermissionSettingsView({ onClose }: { onClose: () => void }) {
+ const intl = useIntl();
const { getExtensions } = useConfig();
const [extensions, setExtensions] = useState([]);
@@ -102,10 +120,9 @@ export default function PermissionSettingsView({ onClose }: { onClose: () => voi
-
Permission Rules
+
{intl.formatMessage(i18n.permissionRules)}
- Hidden instructions that will be passed to the provider to help direct and add context
- to your responses.
+ {intl.formatMessage(i18n.permissionRulesDescription)}
@@ -114,7 +131,7 @@ export default function PermissionSettingsView({ onClose }: { onClose: () => voi
{/* Extension Rules Section */}
{extensions.map((extension) => (
diff --git a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx
index 82043d94..ced389ec 100644
--- a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx
+++ b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx
@@ -13,6 +13,34 @@ import CustomProviderForm from './modal/subcomponents/forms/CustomProviderForm';
import { SwitchModelModal } from '../models/subcomponents/SwitchModelModal';
import { useModelAndProvider } from '../../ModelAndProviderContext';
import type { View } from '../../../utils/navigationUtils';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ addProvider: {
+ id: 'providerGrid.addProvider',
+ defaultMessage: 'Add Provider',
+ },
+ fromTemplateOrManual: {
+ id: 'providerGrid.fromTemplateOrManual',
+ defaultMessage: 'From template or manual setup',
+ },
+ editProvider: {
+ id: 'providerGrid.editProvider',
+ defaultMessage: 'Edit Provider',
+ },
+ configureProvider: {
+ id: 'providerGrid.configureProvider',
+ defaultMessage: 'Configure Provider',
+ },
+ addProviderTitle: {
+ id: 'providerGrid.addProviderTitle',
+ defaultMessage: 'Add Provider',
+ },
+ chooseModel: {
+ id: 'providerGrid.chooseModel',
+ defaultMessage: 'Choose Model',
+ },
+});
const GridLayout = memo(function GridLayout({ children }: { children: React.ReactNode }) {
return (
@@ -29,6 +57,7 @@ const GridLayout = memo(function GridLayout({ children }: { children: React.Reac
});
const CustomProviderCard = memo(function CustomProviderCard({ onClick }: { onClick: () => void }) {
+ const intl = useIntl();
return (
-
Add Provider
-
From template or manual setup
+
{intl.formatMessage(i18n.addProvider)}
+
{intl.formatMessage(i18n.fromTemplateOrManual)}
}
@@ -62,6 +91,7 @@ function ProviderCards({
setView?: (view: View) => void;
onModelSelected?: (model?: string) => void;
}) {
+ const intl = useIntl();
const [configuringProvider, setConfiguringProvider] = useState
(null);
const [showCustomProviderModal, setShowCustomProviderModal] = useState(false);
const [showSwitchModelModal, setShowSwitchModelModal] = useState(false);
@@ -246,7 +276,9 @@ function ProviderCards({
};
const editable = editingProvider ? editingProvider.isEditable : true;
- const title = (editingProvider ? (editable ? 'Edit' : 'Configure') : 'Add') + ' Provider';
+ const title = editingProvider
+ ? (editable ? intl.formatMessage(i18n.editProvider) : intl.formatMessage(i18n.configureProvider))
+ : intl.formatMessage(i18n.addProviderTitle);
return (
<>
{providerCards}
@@ -281,7 +313,7 @@ function ProviderCards({
setView={handleSetView}
onModelSelected={onModelSelected}
initialProvider={switchModelProvider}
- titleOverride="Choose Model"
+ titleOverride={intl.formatMessage(i18n.chooseModel)}
/>
)}
>
diff --git a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx
index 98946090..4dfeb6c6 100644
--- a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx
+++ b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx
@@ -6,6 +6,27 @@ import ProviderGrid from './ProviderGrid';
import { useConfig } from '../../ConfigContext';
import { ProviderDetails } from '../../../api';
import { createNavigationHandler } from '../../../utils/navigationUtils';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ otherProviders: {
+ id: 'providerSettings.otherProviders',
+ defaultMessage: 'Other providers',
+ },
+ configurationSettings: {
+ id: 'providerSettings.configurationSettings',
+ defaultMessage: 'Provider Configuration Settings',
+ },
+ onboardingDescription: {
+ id: 'providerSettings.onboardingDescription',
+ defaultMessage:
+ "Select an AI model provider to get started with goose. You'll need to use API keys generated by each provider which will be encrypted and stored locally. You can change your provider at any time in settings.",
+ },
+ loadingProviders: {
+ id: 'providerSettings.loadingProviders',
+ defaultMessage: 'Loading providers...',
+ },
+});
interface ProviderSettingsProps {
onClose: () => void;
@@ -18,6 +39,7 @@ export default function ProviderSettings({
isOnboarding,
onProviderLaunched,
}: ProviderSettingsProps) {
+ const intl = useIntl();
const { getProviders } = useConfig();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
@@ -67,13 +89,13 @@ export default function ProviderSettings({
- {isOnboarding ? 'Other providers' : 'Provider Configuration Settings'}
+ {isOnboarding
+ ? intl.formatMessage(i18n.otherProviders)
+ : intl.formatMessage(i18n.configurationSettings)}
{isOnboarding && (
- Select an AI model provider to get started with goose. You'll need to use API keys
- generated by each provider which will be encrypted and stored locally. You can
- change your provider at any time in settings.
+ {intl.formatMessage(i18n.onboardingDescription)}
)}
@@ -84,7 +106,7 @@ export default function ProviderSettings({
{loading ? (
-
Loading providers...
+
{intl.formatMessage(i18n.loadingProviders)}
) : (
documentation for more details.',
+ },
+ cancel: {
+ id: 'providerConfigurationModal.cancel',
+ defaultMessage: 'Cancel',
+ },
+ removeConfiguration: {
+ id: 'providerConfigurationModal.removeConfiguration',
+ defaultMessage: 'Remove Configuration',
+ },
+ close: {
+ id: 'providerConfigurationModal.close',
+ defaultMessage: 'Close',
+ },
+});
/** Render a setup step string, turning `backtick` spans into and newlines into . */
function renderSetupStep(text: string) {
@@ -67,6 +157,7 @@ export default function ProviderConfigurationModal({
onClose,
onConfigured,
}: ProviderConfigurationModalProps) {
+ const intl = useIntl();
const [validationErrors, setValidationErrors] = useState>({});
const { upsert, remove } = useConfig();
const { getCurrentModelAndProvider } = useModelAndProvider();
@@ -86,8 +177,8 @@ export default function ProviderConfigurationModal({
const isConfigured = provider.is_configured;
const headerText = showDeleteConfirmation
- ? `Delete configuration for ${provider.metadata.display_name}`
- : `Configure ${provider.metadata.display_name}`;
+ ? intl.formatMessage(i18n.deleteConfigHeader, { providerName: provider.metadata.display_name })
+ : intl.formatMessage(i18n.configureHeader, { providerName: provider.metadata.display_name });
const isExternalSetup =
provider.metadata.config_keys.length === 0 &&
@@ -96,13 +187,13 @@ export default function ProviderConfigurationModal({
const descriptionText = showDeleteConfirmation
? isActiveProvider
- ? `You cannot delete this provider while it's currently in use. Please switch to a different model first.`
- : 'This will permanently delete the current provider configuration.'
+ ? intl.formatMessage(i18n.cannotDeleteActive)
+ : intl.formatMessage(i18n.deleteConfirmation)
: isOAuthProvider
- ? `Sign in with your ${provider.metadata.display_name} account to use this provider`
+ ? intl.formatMessage(i18n.oauthSignInDescription, { providerName: provider.metadata.display_name })
: isExternalSetup
? provider.metadata.description
- : `Add your API key(s) for this provider to integrate into goose`;
+ : intl.formatMessage(i18n.addApiKeyDescription);
const handleOAuthLogin = async () => {
setIsOAuthLoading(true);
@@ -117,7 +208,7 @@ export default function ProviderConfigurationModal({
onClose();
}
} catch (err) {
- setError(`OAuth login failed: ${errorMessage(err)}`);
+ setError(intl.formatMessage(i18n.oauthLoginFailed, { error: errorMessage(err) }));
} finally {
setIsOAuthLoading(false);
}
@@ -137,7 +228,7 @@ export default function ProviderConfigurationModal({
!configValues[parameter.name]?.value &&
!configValues[parameter.name]?.serverValue
) {
- errors[parameter.name] = `${parameter.name} is required`;
+ errors[parameter.name] = intl.formatMessage(i18n.parameterRequired, { paramName: parameter.name });
}
});
@@ -240,15 +331,15 @@ export default function ProviderConfigurationModal({
<>
!open && setError(null)}>
- Error
+ {intl.formatMessage(i18n.errorTitle)}
- There was an error checking this provider configuration.
+ {intl.formatMessage(i18n.errorCheckingConfig)}
{error}
- Check your configuration again to use this provider.
+ {intl.formatMessage(i18n.checkConfigAgain)}
setError(null)}>
- Go Back
+ {intl.formatMessage(i18n.goBack)}
@@ -277,13 +368,13 @@ export default function ProviderConfigurationModal({
>
{isOAuthLoading
- ? 'Signing in...'
- : `Sign in with ${provider.metadata.display_name}`}
+ ? intl.formatMessage(i18n.signingIn)
+ : intl.formatMessage(i18n.signInWith, { providerName: provider.metadata.display_name })}
{provider.metadata.config_keys.some((key) => key.device_code_flow)
- ? 'A browser window will open and the verification code will be copied to your clipboard. Paste it in the browser to complete sign-in.'
- : 'A browser window will open for you to complete the login.'}
+ ? intl.formatMessage(i18n.deviceCodeFlowHint)
+ : intl.formatMessage(i18n.browserWindowHint)}
) : provider.metadata.config_keys.length === 0 &&
@@ -291,7 +382,7 @@ export default function ProviderConfigurationModal({
provider.metadata.setup_steps.length > 0 ? (
@@ -337,11 +430,11 @@ export default function ProviderConfigurationModal({
{isOAuthProvider && !showDeleteConfirmation ? (
- Cancel
+ {intl.formatMessage(i18n.cancel)}
{isConfigured && (
- Remove Configuration
+ {intl.formatMessage(i18n.removeConfiguration)}
)}
@@ -356,7 +449,7 @@ export default function ProviderConfigurationModal({
onClick={handleCancel}
className="w-full h-[60px] rounded-none border-t border-border-primary text-md hover:bg-background-secondary text-text-primary font-medium"
>
- Close
+ {intl.formatMessage(i18n.close)}
) : (
diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderCatalogPicker.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderCatalogPicker.tsx
index 879b299a..8b689ca6 100644
--- a/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderCatalogPicker.tsx
+++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderCatalogPicker.tsx
@@ -9,6 +9,62 @@ import {
type ProviderCatalogEntry,
type ProviderTemplate,
} from '../../../../../api';
+import { defineMessages, useIntl } from '../../../../../i18n';
+
+const i18n = defineMessages({
+ chooseProvider: {
+ id: 'providerCatalogPicker.chooseProvider',
+ defaultMessage: 'Choose Provider',
+ },
+ selectFormatDescription: {
+ id: 'providerCatalogPicker.selectFormatDescription',
+ defaultMessage: "Select an API format and provider. We'll auto-fill the configuration for you.",
+ },
+ apiFormat: {
+ id: 'providerCatalogPicker.apiFormat',
+ defaultMessage: 'API Format',
+ },
+ openaiCompatible: {
+ id: 'providerCatalogPicker.openaiCompatible',
+ defaultMessage: 'OpenAI Compatible',
+ },
+ anthropicCompatible: {
+ id: 'providerCatalogPicker.anthropicCompatible',
+ defaultMessage: 'Anthropic Compatible',
+ },
+ searchProviders: {
+ id: 'providerCatalogPicker.searchProviders',
+ defaultMessage: 'Search providers...',
+ },
+ loadingProviders: {
+ id: 'providerCatalogPicker.loadingProviders',
+ defaultMessage: 'Loading providers...',
+ },
+ errorPrefix: {
+ id: 'providerCatalogPicker.errorPrefix',
+ defaultMessage: 'Error: {error}',
+ },
+ noProvidersFound: {
+ id: 'providerCatalogPicker.noProvidersFound',
+ defaultMessage: 'No providers found for "{query}"',
+ },
+ noProvidersAvailable: {
+ id: 'providerCatalogPicker.noProvidersAvailable',
+ defaultMessage: 'No providers available',
+ },
+ modelsAvailable: {
+ id: 'providerCatalogPicker.modelsAvailable',
+ defaultMessage: '{count} models available',
+ },
+ requiresEnvVar: {
+ id: 'providerCatalogPicker.requiresEnvVar',
+ defaultMessage: ' • Requires {envVar}',
+ },
+ cancel: {
+ id: 'providerCatalogPicker.cancel',
+ defaultMessage: 'Cancel',
+ },
+});
interface ProviderCatalogPickerProps {
onSelect: (template: ProviderTemplate) => void;
@@ -21,6 +77,7 @@ export default function ProviderCatalogPicker({
onCancel,
embedded,
}: ProviderCatalogPickerProps) {
+ const intl = useIntl();
const [selectedFormat, setSelectedFormat] = useState('openai');
const [providers, setProviders] = useState([]);
const [filteredProviders, setFilteredProviders] = useState([]);
@@ -29,8 +86,8 @@ export default function ProviderCatalogPicker({
const [error, setError] = useState(null);
const formatOptions = [
- { value: 'openai', label: 'OpenAI Compatible' },
- { value: 'anthropic', label: 'Anthropic Compatible' },
+ { value: 'openai', label: intl.formatMessage(i18n.openaiCompatible) },
+ { value: 'anthropic', label: intl.formatMessage(i18n.anthropicCompatible) },
];
// Fetch providers when format changes
@@ -91,15 +148,15 @@ export default function ProviderCatalogPicker({
{/* Header */}
-
Choose Provider
+
{intl.formatMessage(i18n.chooseProvider)}
- Select an API format and provider. We'll auto-fill the configuration for you.
+ {intl.formatMessage(i18n.selectFormatDescription)}
{/* Format Selection */}
- API Format
+ {intl.formatMessage(i18n.apiFormat)}
opt.value === selectedFormat)}
@@ -118,7 +175,7 @@ export default function ProviderCatalogPicker({
setSearchQuery(e.target.value)}
className="pl-10"
@@ -126,15 +183,17 @@ export default function ProviderCatalogPicker({
{/* Loading/Error */}
- {loading &&
Loading providers...
}
- {error &&
Error: {error}
}
+ {loading &&
{intl.formatMessage(i18n.loadingProviders)}
}
+ {error &&
{intl.formatMessage(i18n.errorPrefix, { error })}
}
{/* Provider List */}
{!loading && !error && (
{filteredProviders.length === 0 ? (
- {searchQuery ? `No providers found for "${searchQuery}"` : 'No providers available'}
+ {searchQuery
+ ? intl.formatMessage(i18n.noProvidersFound, { query: searchQuery })
+ : intl.formatMessage(i18n.noProvidersAvailable)}
) : (
filteredProviders.map((provider) => (
@@ -161,8 +220,8 @@ export default function ProviderCatalogPicker({
{provider.api_url}
- {provider.model_count} models available
- {provider.env_var && ` • Requires ${provider.env_var}`}
+ {intl.formatMessage(i18n.modelsAvailable, { count: provider.model_count })}
+ {provider.env_var && intl.formatMessage(i18n.requiresEnvVar, { envVar: provider.env_var })}
@@ -177,7 +236,7 @@ export default function ProviderCatalogPicker({
{!embedded && (
- Cancel
+ {intl.formatMessage(i18n.cancel)}
)}
diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderLogo.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderLogo.tsx
index 4e0c3e3a..12a1e8c8 100644
--- a/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderLogo.tsx
+++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderLogo.tsx
@@ -10,6 +10,14 @@ import XaiLogo from './icons/xai@3x.png';
import MiniMaxLogo from './icons/minimax@3x.png';
import TanzuLogo from './icons/tanzu@3x.png';
import DefaultLogo from './icons/default@3x.png';
+import { defineMessages, useIntl } from '../../../../../i18n';
+
+const i18n = defineMessages({
+ logoAlt: {
+ id: 'providerLogo.alt',
+ defaultMessage: '{providerName} logo',
+ },
+});
// Map provider names to their logos
const providerLogos: Record = {
@@ -32,6 +40,7 @@ interface ProviderLogoProps {
}
export default function ProviderLogo({ providerName }: ProviderLogoProps) {
+ const intl = useIntl();
// Convert provider name to lowercase and fetch the logo
const logoKey = providerName.toLowerCase();
const logo = providerLogos[logoKey] || DefaultLogo;
@@ -50,7 +59,7 @@ export default function ProviderLogo({ providerName }: ProviderLogoProps) {
diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderSetupActions.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderSetupActions.tsx
index 121612ce..ae3816c3 100644
--- a/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderSetupActions.tsx
+++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderSetupActions.tsx
@@ -2,6 +2,44 @@ import { SyntheticEvent } from 'react';
import { Button } from '../../../../ui/button';
import { Trash2, AlertTriangle } from 'lucide-react';
import { ConfigKey } from '../../../../../api';
+import { defineMessages, useIntl } from '../../../../../i18n';
+
+const i18n = defineMessages({
+ cannotDeleteActive: {
+ id: 'providerSetupActions.cannotDeleteActive',
+ defaultMessage:
+ 'You cannot delete {providerName} while it\'s currently in use. Please switch to a different model before deleting this provider.',
+ },
+ ok: {
+ id: 'providerSetupActions.ok',
+ defaultMessage: 'Ok',
+ },
+ confirmDeleteMessage: {
+ id: 'providerSetupActions.confirmDeleteMessage',
+ defaultMessage:
+ 'Are you sure you want to delete the configuration parameters for {providerName}? This action cannot be undone.',
+ },
+ confirmDelete: {
+ id: 'providerSetupActions.confirmDelete',
+ defaultMessage: 'Confirm Delete',
+ },
+ cancel: {
+ id: 'providerSetupActions.cancel',
+ defaultMessage: 'Cancel',
+ },
+ deleteProvider: {
+ id: 'providerSetupActions.deleteProvider',
+ defaultMessage: 'Delete Provider',
+ },
+ submit: {
+ id: 'providerSetupActions.submit',
+ defaultMessage: 'Submit',
+ },
+ enableProvider: {
+ id: 'providerSetupActions.enableProvider',
+ defaultMessage: 'Enable Provider',
+ },
+});
interface ProviderSetupActionsProps {
onCancel: () => void;
@@ -32,6 +70,8 @@ export default function ProviderSetupActions({
primaryParameters,
isActiveProvider = false, // Default value provided
}: ProviderSetupActionsProps) {
+ const intl = useIntl();
+
// If we're showing delete confirmation, render the delete confirmation buttons
if (showDeleteConfirmation) {
// Check if this is the active provider
@@ -42,8 +82,7 @@ export default function ProviderSetupActions({
- You cannot delete {providerName} while it's currently in use. Please switch to a
- different model before deleting this provider.
+ {intl.formatMessage(i18n.cannotDeleteActive, { providerName })}
@@ -52,7 +91,7 @@ export default function ProviderSetupActions({
onClick={onCancelDelete}
className="w-full h-[60px] rounded-none hover:bg-background-secondary text-text-secondary hover:text-text-primary text-md font-regular"
>
- Ok
+ {intl.formatMessage(i18n.ok)}
);
@@ -63,22 +102,21 @@ export default function ProviderSetupActions({
- Are you sure you want to delete the configuration parameters for {providerName}? This
- action cannot be undone.
+ {intl.formatMessage(i18n.confirmDeleteMessage, { providerName })}
- Confirm Delete
+ {intl.formatMessage(i18n.confirmDelete)}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
);
@@ -93,7 +131,7 @@ export default function ProviderSetupActions({
onClick={onDelete}
className="w-full h-[60px] rounded-none border-t border-border-primary bg-transparent hover:bg-background-secondary text-red-500 font-medium text-md"
>
- Delete Provider
+ {intl.formatMessage(i18n.deleteProvider)}
)}
{primaryParameters && primaryParameters.length > 0 ? (
@@ -104,7 +142,7 @@ export default function ProviderSetupActions({
onClick={onSubmit}
className="w-full h-[60px] rounded-none border-t border-border-primary text-md hover:bg-background-secondary text-text-primary font-medium"
>
- Submit
+ {intl.formatMessage(i18n.submit)}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
>
) : (
@@ -123,7 +161,7 @@ export default function ProviderSetupActions({
onClick={onSubmit}
className="w-full h-[60px] rounded-none border-t border-border-primary text-md hover:bg-background-secondary text-text-primary font-medium"
>
- Enable Provider
+ {intl.formatMessage(i18n.enableProvider)}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
>
)}
diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/SecureStorageNotice.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/SecureStorageNotice.tsx
index 4548b3aa..e2869e79 100644
--- a/ui/desktop/src/components/settings/providers/modal/subcomponents/SecureStorageNotice.tsx
+++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/SecureStorageNotice.tsx
@@ -1,4 +1,12 @@
import { Lock } from 'lucide-react';
+import { defineMessages, useIntl } from '../../../../../i18n';
+
+const i18n = defineMessages({
+ defaultMessage: {
+ id: 'secureStorageNotice.defaultMessage',
+ defaultMessage: 'Keys are stored securely in the keychain',
+ },
+});
/**
* SecureStorageNotice - A reusable component that displays a message about secure storage
@@ -10,12 +18,17 @@ import { Lock } from 'lucide-react';
*/
export function SecureStorageNotice({
className = '',
- message = 'Keys are stored securely in the keychain',
+ message,
+}: {
+ className?: string;
+ message?: string;
}) {
+ const intl = useIntl();
+ const displayMessage = message ?? intl.formatMessage(i18n.defaultMessage);
return (
- {message}
+ {displayMessage}
);
}
diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/CustomProviderForm.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/CustomProviderForm.tsx
index b105b0f9..593c9471 100644
--- a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/CustomProviderForm.tsx
+++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/CustomProviderForm.tsx
@@ -7,6 +7,221 @@ import { UpdateCustomProviderRequest, type ProviderTemplate } from '../../../../
import { Plus, X, Trash2, AlertTriangle, ExternalLink, Search, Settings } from 'lucide-react';
import { cn } from '../../../../../../utils';
import ProviderCatalogPicker from '../ProviderCatalogPicker';
+import { defineMessages, useIntl } from '../../../../../../i18n';
+
+const i18n = defineMessages({
+ chooseSetup: {
+ id: 'customProviderForm.chooseSetup',
+ defaultMessage: "Choose how you'd like to set up your provider.",
+ },
+ startFromTemplate: {
+ id: 'customProviderForm.startFromTemplate',
+ defaultMessage: 'Start from a provider template',
+ },
+ startFromTemplateDesc: {
+ id: 'customProviderForm.startFromTemplateDesc',
+ defaultMessage: "Pick a known provider and we'll auto-fill the configuration",
+ },
+ configureManually: {
+ id: 'customProviderForm.configureManually',
+ defaultMessage: 'Configure manually',
+ },
+ configureManuallyDesc: {
+ id: 'customProviderForm.configureManuallyDesc',
+ defaultMessage: 'Enter all provider details yourself',
+ },
+ cancel: {
+ id: 'customProviderForm.cancel',
+ defaultMessage: 'Cancel',
+ },
+ back: {
+ id: 'customProviderForm.back',
+ defaultMessage: '← Back',
+ },
+ usingTemplate: {
+ id: 'customProviderForm.usingTemplate',
+ defaultMessage: 'Using template: {name}',
+ },
+ docs: {
+ id: 'customProviderForm.docs',
+ defaultMessage: 'Docs',
+ },
+ clear: {
+ id: 'customProviderForm.clear',
+ defaultMessage: 'Clear',
+ },
+ providerType: {
+ id: 'customProviderForm.providerType',
+ defaultMessage: 'Provider Type',
+ },
+ openaiCompatible: {
+ id: 'customProviderForm.openaiCompatible',
+ defaultMessage: 'OpenAI Compatible',
+ },
+ anthropicCompatible: {
+ id: 'customProviderForm.anthropicCompatible',
+ defaultMessage: 'Anthropic Compatible',
+ },
+ ollamaCompatible: {
+ id: 'customProviderForm.ollamaCompatible',
+ defaultMessage: 'Ollama Compatible',
+ },
+ displayName: {
+ id: 'customProviderForm.displayName',
+ defaultMessage: 'Display Name',
+ },
+ displayNamePlaceholder: {
+ id: 'customProviderForm.displayNamePlaceholder',
+ defaultMessage: 'Your Provider Name',
+ },
+ apiUrl: {
+ id: 'customProviderForm.apiUrl',
+ defaultMessage: 'API URL',
+ },
+ apiUrlPlaceholder: {
+ id: 'customProviderForm.apiUrlPlaceholder',
+ defaultMessage: 'https://api.example.com',
+ },
+ apiBasePath: {
+ id: 'customProviderForm.apiBasePath',
+ defaultMessage: 'API Base Path (optional)',
+ },
+ apiBasePathPlaceholder: {
+ id: 'customProviderForm.apiBasePathPlaceholder',
+ defaultMessage: 'e.g., v1/chat/completions or project_id/v1',
+ },
+ apiBasePathHint: {
+ id: 'customProviderForm.apiBasePathHint',
+ defaultMessage: "Override the default API path. Leave blank to use the provider's default path.",
+ },
+ authentication: {
+ id: 'customProviderForm.authentication',
+ defaultMessage: 'Authentication',
+ },
+ authHint: {
+ id: 'customProviderForm.authHint',
+ defaultMessage: "Local LLMs like Ollama typically don't require an API key.",
+ },
+ requiresApiKey: {
+ id: 'customProviderForm.requiresApiKey',
+ defaultMessage: 'This provider requires an API key',
+ },
+ apiKey: {
+ id: 'customProviderForm.apiKey',
+ defaultMessage: 'API Key',
+ },
+ apiKeyPlaceholderExisting: {
+ id: 'customProviderForm.apiKeyPlaceholderExisting',
+ defaultMessage: 'Leave blank to keep existing key',
+ },
+ apiKeyPlaceholderNew: {
+ id: 'customProviderForm.apiKeyPlaceholderNew',
+ defaultMessage: 'Your API key',
+ },
+ availableModels: {
+ id: 'customProviderForm.availableModels',
+ defaultMessage: 'Available Models (comma-separated)',
+ },
+ modelsPlaceholder: {
+ id: 'customProviderForm.modelsPlaceholder',
+ defaultMessage: 'model-a, model-b, model-c',
+ },
+ toolCalling: {
+ id: 'customProviderForm.toolCalling',
+ defaultMessage: 'Tool calling',
+ },
+ reasoning: {
+ id: 'customProviderForm.reasoning',
+ defaultMessage: 'Reasoning',
+ },
+ attachments: {
+ id: 'customProviderForm.attachments',
+ defaultMessage: 'Attachments',
+ },
+ supportsStreaming: {
+ id: 'customProviderForm.supportsStreaming',
+ defaultMessage: 'Provider supports streaming responses',
+ },
+ customHeaders: {
+ id: 'customProviderForm.customHeaders',
+ defaultMessage: 'Custom Headers',
+ },
+ customHeadersHint: {
+ id: 'customProviderForm.customHeadersHint',
+ defaultMessage:
+ 'Add custom HTTP headers to include in requests to the provider. Click the "+" button to add after filling both fields.',
+ },
+ headerNamePlaceholder: {
+ id: 'customProviderForm.headerNamePlaceholder',
+ defaultMessage: 'Header name',
+ },
+ valuePlaceholder: {
+ id: 'customProviderForm.valuePlaceholder',
+ defaultMessage: 'Value',
+ },
+ add: {
+ id: 'customProviderForm.add',
+ defaultMessage: 'Add',
+ },
+ headerBothRequired: {
+ id: 'customProviderForm.headerBothRequired',
+ defaultMessage: 'Both header name and value must be entered',
+ },
+ headerNoSpaces: {
+ id: 'customProviderForm.headerNoSpaces',
+ defaultMessage: 'Header name cannot contain spaces',
+ },
+ headerDuplicate: {
+ id: 'customProviderForm.headerDuplicate',
+ defaultMessage: 'A header with this name already exists',
+ },
+ displayNameRequired: {
+ id: 'customProviderForm.displayNameRequired',
+ defaultMessage: 'Display name is required',
+ },
+ apiUrlRequired: {
+ id: 'customProviderForm.apiUrlRequired',
+ defaultMessage: 'API URL is required',
+ },
+ apiKeyRequired: {
+ id: 'customProviderForm.apiKeyRequired',
+ defaultMessage: 'API key is required',
+ },
+ modelsRequired: {
+ id: 'customProviderForm.modelsRequired',
+ defaultMessage: 'At least one model is required',
+ },
+ submitError: {
+ id: 'customProviderForm.submitError',
+ defaultMessage: 'Failed to save provider. Please check your configuration and try again.',
+ },
+ cannotDeleteActive: {
+ id: 'customProviderForm.cannotDeleteActive',
+ defaultMessage:
+ "You cannot delete this provider while it's currently in use. Please switch to a different model first.",
+ },
+ deleteConfirmation: {
+ id: 'customProviderForm.deleteConfirmation',
+ defaultMessage:
+ 'Are you sure you want to delete this custom provider? This will permanently remove the provider and its stored API key. This action cannot be undone.',
+ },
+ confirmDelete: {
+ id: 'customProviderForm.confirmDelete',
+ defaultMessage: 'Confirm Delete',
+ },
+ deleteProvider: {
+ id: 'customProviderForm.deleteProvider',
+ defaultMessage: 'Delete Provider',
+ },
+ updateProvider: {
+ id: 'customProviderForm.updateProvider',
+ defaultMessage: 'Update Provider',
+ },
+ createProvider: {
+ id: 'customProviderForm.createProvider',
+ defaultMessage: 'Create Provider',
+ },
+});
type Step = 'choice' | 'catalog' | 'form';
@@ -27,6 +242,7 @@ export default function CustomProviderForm({
initialData,
isEditable,
}: CustomProviderFormProps) {
+ const intl = useIntl();
const [engine, setEngine] = useState('openai_compatible');
const [displayName, setDisplayName] = useState('');
const [apiUrl, setApiUrl] = useState('');
@@ -129,19 +345,19 @@ export default function CustomProviderForm({
if (keyEmpty || valueEmpty) {
setInvalidHeaderFields({ key: keyEmpty, value: valueEmpty });
- setHeaderValidationError('Both header name and value must be entered');
+ setHeaderValidationError(intl.formatMessage(i18n.headerBothRequired));
return;
}
if (keyHasSpaces) {
setInvalidHeaderFields({ key: true, value: false });
- setHeaderValidationError('Header name cannot contain spaces');
+ setHeaderValidationError(intl.formatMessage(i18n.headerNoSpaces));
return;
}
if (isDuplicate) {
setInvalidHeaderFields({ key: true, value: false });
- setHeaderValidationError('A header with this name already exists');
+ setHeaderValidationError(intl.formatMessage(i18n.headerDuplicate));
return;
}
@@ -192,11 +408,11 @@ export default function CustomProviderForm({
setValidationErrors({});
const errors: Record = {};
- if (!displayName) errors.displayName = 'Display name is required';
- if (!apiUrl) errors.apiUrl = 'API URL is required';
+ if (!displayName) errors.displayName = intl.formatMessage(i18n.displayNameRequired);
+ if (!apiUrl) errors.apiUrl = intl.formatMessage(i18n.apiUrlRequired);
const existingHadAuth = initialData && (initialData.requires_auth ?? true);
- if (requiresAuth && !apiKey && !existingHadAuth) errors.apiKey = 'API key is required';
- if (!models) errors.models = 'At least one model is required';
+ if (requiresAuth && !apiKey && !existingHadAuth) errors.apiKey = intl.formatMessage(i18n.apiKeyRequired);
+ if (!models) errors.models = intl.formatMessage(i18n.modelsRequired);
if (Object.keys(errors).length > 0) {
setValidationErrors(errors);
@@ -245,7 +461,7 @@ export default function CustomProviderForm({
});
} catch (error) {
console.error('Failed to save custom provider:', error);
- setSubmitError('Failed to save provider. Please check your configuration and try again.');
+ setSubmitError(intl.formatMessage(i18n.submitError));
}
};
@@ -266,7 +482,7 @@ export default function CustomProviderForm({
if (step === 'choice') {
return (
-
Choose how you'd like to set up your provider.
+
{intl.formatMessage(i18n.chooseSetup)}
setStep('catalog')}
@@ -275,9 +491,9 @@ export default function CustomProviderForm({
-
Start from a provider template
+
{intl.formatMessage(i18n.startFromTemplate)}
- Pick a known provider and we'll auto-fill the configuration
+ {intl.formatMessage(i18n.startFromTemplateDesc)}
@@ -290,16 +506,16 @@ export default function CustomProviderForm({
-
Configure manually
+
{intl.formatMessage(i18n.configureManually)}
- Enter all provider details yourself
+ {intl.formatMessage(i18n.configureManuallyDesc)}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
@@ -313,10 +529,10 @@ export default function CustomProviderForm({
setStep('choice')}>
- ← Back
+ {intl.formatMessage(i18n.back)}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
@@ -332,7 +548,7 @@ export default function CustomProviderForm({
- Using template: {selectedTemplate.name}
+ {intl.formatMessage(i18n.usingTemplate, { name: selectedTemplate.name })}
{selectedTemplate.api_url}
@@ -344,7 +560,7 @@ export default function CustomProviderForm({
rel="noopener noreferrer"
className="text-primary hover:underline text-sm flex items-center gap-1"
>
- Docs
+ {intl.formatMessage(i18n.docs)}
)}
- Clear
+ {intl.formatMessage(i18n.clear)}
@@ -364,7 +580,7 @@ export default function CustomProviderForm({
{/* Back to choice (create without template only) */}
{!initialData && !selectedTemplate && (
setStep('choice')}>
- ← Back
+ {intl.formatMessage(i18n.back)}
)}
@@ -375,7 +591,7 @@ export default function CustomProviderForm({
htmlFor="provider-select"
className="flex items-center text-sm font-medium text-text-primary mb-2"
>
- Provider Type
+ {intl.formatMessage(i18n.providerType)}
*
{
const selectedOption = option as { value: string; label: string } | null;
@@ -417,14 +633,14 @@ export default function CustomProviderForm({
htmlFor="display-name"
className="flex items-center text-sm font-medium text-text-primary mb-2"
>
- Display Name
+ {intl.formatMessage(i18n.displayName)}
*
setDisplayName(e.target.value)}
- placeholder="Your Provider Name"
+ placeholder={intl.formatMessage(i18n.displayNamePlaceholder)}
aria-invalid={!!validationErrors.displayName}
aria-describedby={validationErrors.displayName ? 'display-name-error' : undefined}
className={validationErrors.displayName ? 'border-red-500' : ''}
@@ -444,14 +660,14 @@ export default function CustomProviderForm({
htmlFor="api-url"
className="flex items-center text-sm font-medium text-text-primary mb-2"
>
- API URL
+ {intl.formatMessage(i18n.apiUrl)}
*
setApiUrl(e.target.value)}
- placeholder="https://api.example.com"
+ placeholder={intl.formatMessage(i18n.apiUrlPlaceholder)}
aria-invalid={!!validationErrors.apiUrl}
aria-describedby={validationErrors.apiUrl ? 'api-url-error' : undefined}
className={validationErrors.apiUrl ? 'border-red-500' : ''}
@@ -471,25 +687,25 @@ export default function CustomProviderForm({
htmlFor="base-path"
className="flex items-center text-sm font-medium text-text-primary mb-2"
>
- API Base Path (optional)
+ {intl.formatMessage(i18n.apiBasePath)}
setBasePath(e.target.value)}
- placeholder="e.g., v1/chat/completions or project_id/v1"
+ placeholder={intl.formatMessage(i18n.apiBasePathPlaceholder)}
/>
- Override the default API path. Leave blank to use the provider's default path.
+ {intl.formatMessage(i18n.apiBasePathHint)}
)}
{/* Authentication */}
-
Authentication
+
{intl.formatMessage(i18n.authentication)}
- Local LLMs like Ollama typically don't require an API key.
+ {intl.formatMessage(i18n.authHint)}
- This provider requires an API key
+ {intl.formatMessage(i18n.requiresApiKey)}
@@ -510,7 +726,7 @@ export default function CustomProviderForm({
htmlFor="api-key"
className="flex items-center text-sm font-medium text-text-primary mb-2"
>
- API Key
+ {intl.formatMessage(i18n.apiKey)}
{selectedTemplate?.env_var && (
({selectedTemplate.env_var})
@@ -523,7 +739,7 @@ export default function CustomProviderForm({
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
- placeholder={initialData ? 'Leave blank to keep existing key' : 'Your API key'}
+ placeholder={initialData ? intl.formatMessage(i18n.apiKeyPlaceholderExisting) : intl.formatMessage(i18n.apiKeyPlaceholderNew)}
aria-invalid={!!validationErrors.apiKey}
aria-describedby={validationErrors.apiKey ? 'api-key-error' : undefined}
className={validationErrors.apiKey ? 'border-red-500' : ''}
@@ -544,14 +760,14 @@ export default function CustomProviderForm({
htmlFor="available-models"
className="flex items-center text-sm font-medium text-text-primary mb-2"
>
- Available Models (comma-separated)
+ {intl.formatMessage(i18n.availableModels)}
*
setModels(e.target.value)}
- placeholder="model-a, model-b, model-c"
+ placeholder={intl.formatMessage(i18n.modelsPlaceholder)}
aria-invalid={!!validationErrors.models}
aria-describedby={validationErrors.models ? 'available-models-error' : undefined}
className={validationErrors.models ? 'border-red-500' : ''}
@@ -566,17 +782,17 @@ export default function CustomProviderForm({
{templateModelCapabilities.tool_call && (
- Tool calling
+ {intl.formatMessage(i18n.toolCalling)}
)}
{templateModelCapabilities.reasoning && (
- Reasoning
+ {intl.formatMessage(i18n.reasoning)}
)}
{templateModelCapabilities.attachment && (
- Attachments
+ {intl.formatMessage(i18n.attachments)}
)}
@@ -595,7 +811,7 @@ export default function CustomProviderForm({
className="rounded border-border-primary"
/>
- Provider supports streaming responses
+ {intl.formatMessage(i18n.supportsStreaming)}
)}
@@ -603,10 +819,9 @@ export default function CustomProviderForm({
{/* Custom headers */}
{isEditable && (
-
Custom Headers
+
{intl.formatMessage(i18n.customHeaders)}
- Add custom HTTP headers to include in requests to the provider. Click the "+" button to
- add after filling both fields.
+ {intl.formatMessage(i18n.customHeadersHint)}
{headerValidationError && (
@@ -686,16 +901,14 @@ export default function CustomProviderForm({
- You cannot delete this provider while it's currently in use. Please switch to a
- different model first.
+ {intl.formatMessage(i18n.cannotDeleteActive)}
) : (
- Are you sure you want to delete this custom provider? This will permanently remove
- the provider and its stored API key. This action cannot be undone.
+ {intl.formatMessage(i18n.deleteConfirmation)}
)}
@@ -705,12 +918,12 @@ export default function CustomProviderForm({
variant="outline"
onClick={() => setShowDeleteConfirmation(false)}
>
- Cancel
+ {intl.formatMessage(i18n.cancel)}
{!isActiveProvider && (
- Confirm Delete
+ {intl.formatMessage(i18n.confirmDelete)}
)}
@@ -725,13 +938,13 @@ export default function CustomProviderForm({
onClick={() => setShowDeleteConfirmation(true)}
>
- Delete Provider
+ {intl.formatMessage(i18n.deleteProvider)}
)}
- Cancel
+ {intl.formatMessage(i18n.cancel)}
- {initialData ? 'Update Provider' : 'Create Provider'}
+ {initialData ? intl.formatMessage(i18n.updateProvider) : intl.formatMessage(i18n.createProvider)}
)}
diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx
index f8c61cbb..573667e9 100644
--- a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx
+++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx
@@ -3,6 +3,50 @@ import { Input } from '../../../../../ui/input';
import { useConfig } from '../../../../../ConfigContext';
import { ProviderDetails, ConfigKey } from '../../../../../../api';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../../../../ui/collapsible';
+import { defineMessages, useIntl } from '../../../../../../i18n';
+
+const i18n = defineMessages({
+ loadingConfig: {
+ id: 'defaultProviderSetupForm.loadingConfig',
+ defaultMessage: 'Loading configuration values...',
+ },
+ noConfigParameters: {
+ id: 'defaultProviderSetupForm.noConfigParameters',
+ defaultMessage: 'No configuration parameters for this provider.',
+ },
+ apiKeyPlaceholder: {
+ id: 'defaultProviderSetupForm.apiKeyPlaceholder',
+ defaultMessage: 'Your API key',
+ },
+ apiHostPlaceholder: {
+ id: 'defaultProviderSetupForm.apiHostPlaceholder',
+ defaultMessage: 'https://api.example.com',
+ },
+ modelsPlaceholder: {
+ id: 'defaultProviderSetupForm.modelsPlaceholder',
+ defaultMessage: 'model-a, model-b',
+ },
+ apiKeyLabel: {
+ id: 'defaultProviderSetupForm.apiKeyLabel',
+ defaultMessage: 'API Key',
+ },
+ apiHostLabel: {
+ id: 'defaultProviderSetupForm.apiHostLabel',
+ defaultMessage: 'API Host',
+ },
+ modelsLabel: {
+ id: 'defaultProviderSetupForm.modelsLabel',
+ defaultMessage: 'Models',
+ },
+ showOptions: {
+ id: 'defaultProviderSetupForm.showOptions',
+ defaultMessage: 'Show {count} options',
+ },
+ hideOptions: {
+ id: 'defaultProviderSetupForm.hideOptions',
+ defaultMessage: 'Hide {count} options',
+ },
+});
type ValidationErrors = Record;
@@ -47,6 +91,7 @@ export default function DefaultProviderSetupForm({
() => provider.metadata.config_keys || [],
[provider.metadata.config_keys]
);
+ const intl = useIntl();
const [isLoading, setIsLoading] = useState(true);
const [optionalExpanded, setOptionalExpanded] = useState(false);
const { read } = useConfig();
@@ -94,9 +139,9 @@ export default function DefaultProviderSetupForm({
}
const name = parameter.name.toLowerCase();
- if (name.includes('api_key')) return 'Your API key';
- if (name.includes('api_url') || name.includes('host')) return 'https://api.example.com';
- if (name.includes('models')) return 'model-a, model-b';
+ if (name.includes('api_key')) return intl.formatMessage(i18n.apiKeyPlaceholder);
+ if (name.includes('api_url') || name.includes('host')) return intl.formatMessage(i18n.apiHostPlaceholder);
+ if (name.includes('models')) return intl.formatMessage(i18n.modelsPlaceholder);
return parameter.name
.replace(/_/g, ' ')
@@ -106,9 +151,9 @@ export default function DefaultProviderSetupForm({
const getFieldLabel = (parameter: ConfigKey) => {
const name = parameter.name.toLowerCase();
- if (name.includes('api_key')) return 'API Key';
- if (name.includes('api_url') || name.includes('host')) return 'API Host';
- if (name.includes('models')) return 'Models';
+ if (name.includes('api_key')) return intl.formatMessage(i18n.apiKeyLabel);
+ if (name.includes('api_url') || name.includes('host')) return intl.formatMessage(i18n.apiHostLabel);
+ if (name.includes('models')) return intl.formatMessage(i18n.modelsLabel);
let parameter_name = parameter.name.toUpperCase();
if (parameter_name.startsWith(provider.name.toUpperCase().replace('-', '_'))) {
@@ -124,7 +169,7 @@ export default function DefaultProviderSetupForm({
};
if (isLoading) {
- return Loading configuration values...
;
+ return {intl.formatMessage(i18n.loadingConfig)}
;
}
function getRenderValue(parameter: ConfigKey): string {
@@ -244,13 +289,15 @@ export default function DefaultProviderSetupForm({
belowFoldParameters = [];
}
- const expandCtaText = `${optionalExpanded ? 'Hide' : 'Show'} ${belowFoldParameters.length} options `;
+ const expandCtaText = optionalExpanded
+ ? intl.formatMessage(i18n.hideOptions, { count: belowFoldParameters.length })
+ : intl.formatMessage(i18n.showOptions, { count: belowFoldParameters.length });
return (
{aboveFoldParameters.length === 0 && belowFoldParameters.length === 0 ? (
- No configuration parameters for this provider.
+ {intl.formatMessage(i18n.noConfigParameters)}
) : (
diff --git a/ui/desktop/src/components/settings/providers/subcomponents/CardHeader.tsx b/ui/desktop/src/components/settings/providers/subcomponents/CardHeader.tsx
index 76840fbd..b16de9fd 100644
--- a/ui/desktop/src/components/settings/providers/subcomponents/CardHeader.tsx
+++ b/ui/desktop/src/components/settings/providers/subcomponents/CardHeader.tsx
@@ -1,6 +1,7 @@
import { memo } from 'react';
import { GreenCheckButton } from './buttons/CardButtons';
import { ConfiguredProviderTooltipMessage, ProviderDescription } from './utils/StringUtils';
+import { useIntl } from '../../../../i18n';
interface CardHeaderProps {
name: string;
@@ -21,12 +22,13 @@ interface ProviderNameAndStatusProps {
}
const ProviderNameAndStatus = memo(({ name, isConfigured }: ProviderNameAndStatusProps) => {
+ const intl = useIntl();
return (
{/* Configured state: Green check */}
- {isConfigured && }
+ {isConfigured && }
);
});
diff --git a/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx b/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx
index 4be31bdf..4b9baf5e 100644
--- a/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx
+++ b/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx
@@ -4,6 +4,18 @@ import CardHeader from './CardHeader';
import CardBody from './CardBody';
import DefaultCardButtons from './buttons/DefaultCardButtons';
import { ProviderDetails, ProviderMetadata } from '../../../../api';
+import { defineMessages, useIntl } from '../../../../i18n';
+
+const i18n = defineMessages({
+ noMetadata: {
+ id: 'providerCard.noMetadata',
+ defaultMessage: 'ProviderCard error: No metadata provided',
+ },
+ unknownProvider: {
+ id: 'providerCard.unknownProvider',
+ defaultMessage: 'Unknown Provider',
+ },
+});
type ProviderCardProps = {
provider: ProviderDetails;
@@ -18,6 +30,7 @@ export const ProviderCard = function ProviderCard({
onLaunch,
isOnboarding,
}: ProviderCardProps) {
+ const intl = useIntl();
// Safely access metadata with null checks
const providerMetadata: ProviderMetadata | null = provider?.metadata || null;
@@ -25,7 +38,7 @@ export const ProviderCard = function ProviderCard({
const metadata = useMemo(() => providerMetadata, [providerMetadata]);
if (!metadata) {
- return
ProviderCard error: No metadata provided
;
+ return
{intl.formatMessage(i18n.noMetadata)}
;
}
const handleCardClick = () => {
@@ -41,7 +54,7 @@ export const ProviderCard = function ProviderCard({
onClick={handleCardClick}
header={
diff --git a/ui/desktop/src/components/settings/providers/subcomponents/buttons/CardButtons.tsx b/ui/desktop/src/components/settings/providers/subcomponents/buttons/CardButtons.tsx
index d4eccd2d..de8c0d9a 100644
--- a/ui/desktop/src/components/settings/providers/subcomponents/buttons/CardButtons.tsx
+++ b/ui/desktop/src/components/settings/providers/subcomponents/buttons/CardButtons.tsx
@@ -3,6 +3,18 @@ import { Button } from '../../../../ui/button';
import clsx from 'clsx';
import { TooltipWrapper } from './TooltipWrapper';
import { Check, Rocket, Sliders } from 'lucide-react';
+import { defineMessages, useIntl } from '../../../../../i18n';
+
+const i18n = defineMessages({
+ configure: {
+ id: 'cardButtons.configure',
+ defaultMessage: 'Configure',
+ },
+ launch: {
+ id: 'cardButtons.launch',
+ defaultMessage: 'Launch',
+ },
+});
interface ActionButtonProps extends React.ComponentProps
{
/** Icon component to render, e.g. `RefreshCw` from lucide-react */
@@ -65,12 +77,13 @@ export function GreenCheckButton({ tooltip, className = '', ...props }: ActionBu
}
export function ConfigureSettingsButton({ tooltip, className, ...props }: ActionButtonProps) {
+ const intl = useIntl();
return (
diff --git a/ui/desktop/src/components/settings/providers/subcomponents/buttons/DefaultCardButtons.tsx b/ui/desktop/src/components/settings/providers/subcomponents/buttons/DefaultCardButtons.tsx
index a22f4a7c..7bd7033f 100644
--- a/ui/desktop/src/components/settings/providers/subcomponents/buttons/DefaultCardButtons.tsx
+++ b/ui/desktop/src/components/settings/providers/subcomponents/buttons/DefaultCardButtons.tsx
@@ -1,5 +1,25 @@
import { ConfigureSettingsButton, RocketButton } from './CardButtons';
import { ProviderDetails } from '../../../../../api';
+import { defineMessages, useIntl } from '../../../../../i18n';
+
+const i18n = defineMessages({
+ configureSettings: {
+ id: 'defaultCardButtons.configureSettings',
+ defaultMessage: 'Configure {name} settings',
+ },
+ editSettings: {
+ id: 'defaultCardButtons.editSettings',
+ defaultMessage: 'Edit {name} settings',
+ },
+ deleteSettings: {
+ id: 'defaultCardButtons.deleteSettings',
+ defaultMessage: 'Delete {name} settings',
+ },
+ getStarted: {
+ id: 'defaultCardButtons.getStarted',
+ defaultMessage: 'Get started with goose!',
+ },
+});
// can define other optional callbacks as needed
interface CardButtonsProps {
@@ -9,31 +29,21 @@ interface CardButtonsProps {
onLaunch: (provider: ProviderDetails) => void;
}
-function getDefaultTooltipMessages(name: string, actionType: string) {
- switch (actionType) {
- case 'add':
- return `Configure ${name} settings`;
- case 'edit':
- return `Edit ${name} settings`;
- case 'delete':
- return `Delete ${name} settings`;
- default:
- return null;
- }
-}
-
export default function DefaultCardButtons({
provider,
isOnboardingPage,
onLaunch,
onConfigure,
}: CardButtonsProps) {
+ const intl = useIntl();
+ const name = provider.metadata.display_name;
+
return (
<>
{/*Set up an unconfigured provider */}
{!provider.is_configured && (
{
e.stopPropagation();
onConfigure(provider);
@@ -43,7 +53,7 @@ export default function DefaultCardButtons({
{/*show edit tooltip instead when hovering over button for configured providers*/}
{provider.is_configured && !isOnboardingPage && (
{
e.stopPropagation();
onConfigure(provider);
@@ -53,7 +63,7 @@ export default function DefaultCardButtons({
{/*show Launch button for configured providers on onboarding page*/}
{provider.is_configured && isOnboardingPage && (
{
e.stopPropagation();
onLaunch(provider);
diff --git a/ui/desktop/src/components/settings/providers/subcomponents/utils/StringUtils.tsx b/ui/desktop/src/components/settings/providers/subcomponents/utils/StringUtils.tsx
index ad6cf78c..e3f4562a 100644
--- a/ui/desktop/src/components/settings/providers/subcomponents/utils/StringUtils.tsx
+++ b/ui/desktop/src/components/settings/providers/subcomponents/utils/StringUtils.tsx
@@ -1,23 +1,46 @@
+import { defineMessages, useIntl } from '../../../../../i18n';
+import type { IntlShape } from 'react-intl';
+
+const i18n = defineMessages({
+ ollamaNotConfiguredPrefix: {
+ id: 'stringUtils.ollamaNotConfiguredPrefix',
+ defaultMessage: 'To use, either the',
+ },
+ ollamaApp: {
+ id: 'stringUtils.ollamaApp',
+ defaultMessage: 'Ollama app',
+ },
+ ollamaNotConfiguredSuffix: {
+ id: 'stringUtils.ollamaNotConfiguredSuffix',
+ defaultMessage: 'must be installed on your machine and open, or you must enter a value for OLLAMA_HOST.',
+ },
+ configuredProvider: {
+ id: 'stringUtils.configuredProvider',
+ defaultMessage: '{name} provider is configured',
+ },
+});
+
// Functions for string / string-based element creation (e.g. tooltips for each provider, descriptions, etc)
export function OllamaNotConfiguredTooltipMessage() {
+ const intl = useIntl();
return (
- To use, either the{' '}
+ {intl.formatMessage(i18n.ollamaNotConfiguredPrefix)}{' '}
- Ollama app
+ {intl.formatMessage(i18n.ollamaApp)}
{' '}
- must be installed on your machine and open, or you must enter a value for OLLAMA_HOST.
+ {intl.formatMessage(i18n.ollamaNotConfiguredSuffix)}
);
}
-export function ConfiguredProviderTooltipMessage(name: string) {
- return `${name} provider is configured`;
+export function ConfiguredProviderTooltipMessage(intl: IntlShape, name: string) {
+ return intl.formatMessage(i18n.configuredProvider, { name });
}
interface ProviderDescriptionProps {
diff --git a/ui/desktop/src/components/settings/reset_provider/ResetProviderSection.tsx b/ui/desktop/src/components/settings/reset_provider/ResetProviderSection.tsx
index ef990785..3a457e1f 100644
--- a/ui/desktop/src/components/settings/reset_provider/ResetProviderSection.tsx
+++ b/ui/desktop/src/components/settings/reset_provider/ResetProviderSection.tsx
@@ -2,12 +2,25 @@ import { Button } from '../../ui/button';
import { RefreshCw } from 'lucide-react';
import { useConfig } from '../../ConfigContext';
import { View, ViewOptions } from '../../../utils/navigationUtils';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ resetButton: {
+ id: 'resetProviderSection.resetButton',
+ defaultMessage: 'Reset Provider and Model',
+ },
+ resetDescription: {
+ id: 'resetProviderSection.resetDescription',
+ defaultMessage: "This will clear your selected model and provider settings. If no defaults are available, you'll be taken to the welcome screen to set them up again.",
+ },
+});
interface ResetProviderSectionProps {
setView: (view: View, viewOptions?: ViewOptions) => void;
}
export default function ResetProviderSection(_props: ResetProviderSectionProps) {
+ const intl = useIntl();
const { remove } = useConfig();
const handleResetProvider = async () => {
@@ -29,11 +42,10 @@ export default function ResetProviderSection(_props: ResetProviderSectionProps)
className="flex items-center justify-center gap-2"
>
- Reset Provider and Model
+ {intl.formatMessage(i18n.resetButton)}
- This will clear your selected model and provider settings. If no defaults are available,
- you'll be taken to the welcome screen to set them up again.
+ {intl.formatMessage(i18n.resetDescription)}
);
diff --git a/ui/desktop/src/components/settings/response_styles/ResponseStyleSelectionItem.tsx b/ui/desktop/src/components/settings/response_styles/ResponseStyleSelectionItem.tsx
index 323e51ab..10299961 100644
--- a/ui/desktop/src/components/settings/response_styles/ResponseStyleSelectionItem.tsx
+++ b/ui/desktop/src/components/settings/response_styles/ResponseStyleSelectionItem.tsx
@@ -1,21 +1,42 @@
import { useEffect, useState } from 'react';
+import { defineMessages, useIntl } from '../../../i18n';
+import type { MessageDescriptor } from 'react-intl';
+
+const i18n = defineMessages({
+ detailedLabel: {
+ id: 'responseStyle.detailedLabel',
+ defaultMessage: 'Detailed',
+ },
+ detailedDescription: {
+ id: 'responseStyle.detailedDescription',
+ defaultMessage: 'Tool calls are by default shown open to expose details',
+ },
+ conciseLabel: {
+ id: 'responseStyle.conciseLabel',
+ defaultMessage: 'Concise',
+ },
+ conciseDescription: {
+ id: 'responseStyle.conciseDescription',
+ defaultMessage: 'Tool calls are by default closed and only show the tool used',
+ },
+});
export interface ResponseStyle {
key: string;
- label: string;
- description: string;
+ label: MessageDescriptor;
+ description: MessageDescriptor;
}
export const all_response_styles: ResponseStyle[] = [
{
key: 'detailed',
- label: 'Detailed',
- description: 'Tool calls are by default shown open to expose details',
+ label: i18n.detailedLabel,
+ description: i18n.detailedDescription,
},
{
key: 'concise',
- label: 'Concise',
- description: 'Tool calls are by default closed and only show the tool used',
+ label: i18n.conciseLabel,
+ description: i18n.conciseDescription,
},
];
@@ -32,6 +53,7 @@ export function ResponseStyleSelectionItem({
showDescription,
handleStyleChange,
}: ResponseStyleSelectionItemProps) {
+ const intl = useIntl();
const [checked, setChecked] = useState(currentStyle === style.key);
useEffect(() => {
@@ -46,9 +68,9 @@ export function ResponseStyleSelectionItem({
>
-
{style.label}
+
{intl.formatMessage(style.label)}
{showDescription && (
-
{style.description}
+
{intl.formatMessage(style.description)}
)}
@@ -63,7 +85,7 @@ export function ResponseStyleSelectionItem({
className="peer sr-only"
/>
{
return (
@@ -96,6 +172,7 @@ const ClassifierEndpointInputs = ({
};
export const SecurityToggle = () => {
+ const intl = useIntl();
const { config, upsert } = useConfig();
const modelMapping = useMemo(() => {
@@ -217,9 +294,9 @@ export const SecurityToggle = () => {
-
Enable Prompt Injection Detection
+
{intl.formatMessage(i18n.enablePromptInjection)}
- Detect and prevent potential prompt injection attacks
+ {intl.formatMessage(i18n.promptInjectionDescription)}
@@ -238,10 +315,10 @@ export const SecurityToggle = () => {
- Detection Threshold
+ {intl.formatMessage(i18n.detectionThreshold)}
- Higher values are more strict (0.01 = very lenient, 1.0 = maximum strict)
+ {intl.formatMessage(i18n.thresholdDescription)}
{
- Enable Command Injection ML Detection
+ {intl.formatMessage(i18n.enableCommandInjection)}
- Use ML models to detect malicious shell commands
+ {intl.formatMessage(i18n.commandInjectionDescription)}
@@ -298,7 +375,7 @@ export const SecurityToggle = () => {
enabled &&
effectiveCommandClassifierEnabled && (
- ✓ Command classifier active (auto-configured from environment)
+ ✓ {intl.formatMessage(i18n.commandClassifierActive)}
)
) : (
@@ -320,7 +397,10 @@ export const SecurityToggle = () => {
disabled={!enabled || !effectiveCommandClassifierEnabled}
endpointPlaceholder="https://example.com/classify"
tokenPlaceholder="token..."
- endpointDescription="Enter the full URL for your command injection classification service"
+ endpointLabel={intl.formatMessage(i18n.classificationEndpoint)}
+ endpointDescription={intl.formatMessage(i18n.commandEndpointDescription)}
+ tokenLabel={intl.formatMessage(i18n.apiTokenOptional)}
+ tokenDescription={intl.formatMessage(i18n.apiTokenDescription)}
/>
@@ -334,10 +414,10 @@ export const SecurityToggle = () => {
- Enable Prompt Injection ML Detection
+ {intl.formatMessage(i18n.enablePromptInjectionMl)}
- Use ML models to detect potential prompt injection in your chat
+ {intl.formatMessage(i18n.promptInjectionMlDescription)}
@@ -363,10 +443,10 @@ export const SecurityToggle = () => {
- Detection Model
+ {intl.formatMessage(i18n.detectionModel)}
- Select which ML model to use for prompt injection detection
+ {intl.formatMessage(i18n.detectionModelDescription)}
{
disabled={!enabled || !mlEnabled}
endpointPlaceholder="https://router.huggingface.co/hf-inference/models/protectai/deberta-v3-base-prompt-injection-v2"
tokenPlaceholder="hf_..."
- endpointDescription="Enter the full URL for your ML classification service (including model identifier)"
- tokenDescription="Authentication token for the ML service (e.g., HuggingFace token)"
+ endpointLabel={intl.formatMessage(i18n.classificationEndpoint)}
+ endpointDescription={intl.formatMessage(i18n.mlEndpointDescription)}
+ tokenLabel={intl.formatMessage(i18n.apiTokenOptional)}
+ tokenDescription={intl.formatMessage(i18n.mlTokenDescription)}
/>
)}
diff --git a/ui/desktop/src/components/settings/sessions/SessionSharingSection.tsx b/ui/desktop/src/components/settings/sessions/SessionSharingSection.tsx
index 780bb8e6..c988b009 100644
--- a/ui/desktop/src/components/settings/sessions/SessionSharingSection.tsx
+++ b/ui/desktop/src/components/settings/sessions/SessionSharingSection.tsx
@@ -5,8 +5,85 @@ import { Switch } from '../../ui/switch';
import { Button } from '../../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { trackSettingToggled } from '../../../utils/analytics';
+import { defineMessages, useIntl } from '../../../i18n';
+
+const i18n = defineMessages({
+ title: {
+ id: 'sessionSharingSection.title',
+ defaultMessage: 'Session Sharing',
+ },
+ descriptionConfigured: {
+ id: 'sessionSharingSection.descriptionConfigured',
+ defaultMessage:
+ 'Session sharing is configured but fully opt-in — your sessions are only shared when you explicitly click the share button.',
+ },
+ descriptionDefault: {
+ id: 'sessionSharingSection.descriptionDefault',
+ defaultMessage: 'You can enable session sharing to share your sessions with others.',
+ },
+ alreadyConfigured: {
+ id: 'sessionSharingSection.alreadyConfigured',
+ defaultMessage: 'Session sharing has already been configured',
+ },
+ enableSharing: {
+ id: 'sessionSharingSection.enableSharing',
+ defaultMessage: 'Enable session sharing',
+ },
+ baseUrl: {
+ id: 'sessionSharingSection.baseUrl',
+ defaultMessage: 'Base URL',
+ },
+ urlPlaceholder: {
+ id: 'sessionSharingSection.urlPlaceholder',
+ defaultMessage: 'https://example.com/api',
+ },
+ invalidUrl: {
+ id: 'sessionSharingSection.invalidUrl',
+ defaultMessage:
+ 'Invalid URL format. Please enter a valid URL (e.g. https://example.com/api).',
+ },
+ testingConnection: {
+ id: 'sessionSharingSection.testingConnection',
+ defaultMessage: 'Testing connection...',
+ },
+ connectionSuccess: {
+ id: 'sessionSharingSection.connectionSuccess',
+ defaultMessage: 'Connection successful!',
+ },
+ serverError: {
+ id: 'sessionSharingSection.serverError',
+ defaultMessage:
+ 'Server error: HTTP {status}. The server may not be configured correctly.',
+ },
+ connectionFailed: {
+ id: 'sessionSharingSection.connectionFailed',
+ defaultMessage: 'Connection failed. ',
+ },
+ unreachableServer: {
+ id: 'sessionSharingSection.unreachableServer',
+ defaultMessage:
+ 'Unable to reach the server. Please check the URL and your network connection.',
+ },
+ connectionTimedOut: {
+ id: 'sessionSharingSection.connectionTimedOut',
+ defaultMessage: 'Connection timed out. The server may be slow or unreachable.',
+ },
+ unknownError: {
+ id: 'sessionSharingSection.unknownError',
+ defaultMessage: 'Unknown error occurred.',
+ },
+ testing: {
+ id: 'sessionSharingSection.testing',
+ defaultMessage: 'Testing...',
+ },
+ testConnection: {
+ id: 'sessionSharingSection.testConnection',
+ defaultMessage: 'Test Connection',
+ },
+});
export default function SessionSharingSection() {
+ const intl = useIntl();
const envBaseUrlShare = window.appConfig.get('GOOSE_BASE_URL_SHARE');
// If env is set, force sharing enabled and set the baseUrl accordingly.
@@ -80,7 +157,7 @@ export default function SessionSharingSection() {
const updated = { ...sessionSharingConfig, baseUrl: newBaseUrl };
await window.electron.setSetting('sessionSharing', updated);
} else {
- setUrlError('Invalid URL format. Please enter a valid URL (e.g. https://example.com/api).');
+ setUrlError(intl.formatMessage(i18n.invalidUrl));
}
};
@@ -89,7 +166,7 @@ export default function SessionSharingSection() {
const baseUrl = sessionSharingConfig.baseUrl;
if (!baseUrl) return;
- setTestResult({ status: 'testing', message: 'Testing connection...' });
+ setTestResult({ status: 'testing', message: intl.formatMessage(i18n.testingConnection) });
try {
// Create an AbortController for timeout
@@ -111,29 +188,28 @@ export default function SessionSharingSection() {
if (response.status < 500) {
setTestResult({
status: 'success',
- message: 'Connection successful!',
+ message: intl.formatMessage(i18n.connectionSuccess),
});
} else {
setTestResult({
status: 'error',
- message: `Server error: HTTP ${response.status}. The server may not be configured correctly.`,
+ message: intl.formatMessage(i18n.serverError, { status: response.status }),
});
}
} catch (error) {
console.error('Connection test failed:', error);
- let errorMessage = 'Connection failed. ';
+ let errorMessage = intl.formatMessage(i18n.connectionFailed);
if (error instanceof TypeError && error.message.includes('fetch')) {
- errorMessage +=
- 'Unable to reach the server. Please check the URL and your network connection.';
+ errorMessage += intl.formatMessage(i18n.unreachableServer);
} else if (error instanceof Error) {
if (error.name === 'AbortError') {
- errorMessage += 'Connection timed out. The server may be slow or unreachable.';
+ errorMessage += intl.formatMessage(i18n.connectionTimedOut);
} else {
errorMessage += error.message;
}
} else {
- errorMessage += 'Unknown error occurred.';
+ errorMessage += intl.formatMessage(i18n.unknownError);
}
setTestResult({
@@ -147,11 +223,11 @@ export default function SessionSharingSection() {
- Session Sharing
+ {intl.formatMessage(i18n.title)}
{(envBaseUrlShare as string)
- ? 'Session sharing is configured but fully opt-in — your sessions are only shared when you explicitly click the share button.'
- : 'You can enable session sharing to share your sessions with others.'}
+ ? intl.formatMessage(i18n.descriptionConfigured)
+ : intl.formatMessage(i18n.descriptionDefault)}
@@ -160,8 +236,8 @@ export default function SessionSharingSection() {
{(envBaseUrlShare as string)
- ? 'Session sharing has already been configured'
- : 'Enable session sharing'}
+ ? intl.formatMessage(i18n.alreadyConfigured)
+ : intl.formatMessage(i18n.enableSharing)}
{envBaseUrlShare ? (
@@ -181,7 +257,7 @@ export default function SessionSharingSection() {
- Base URL
+ {intl.formatMessage(i18n.baseUrl)}
{isUrlConfigured && }
@@ -189,7 +265,7 @@ export default function SessionSharingSection() {
- Testing...
+ {intl.formatMessage(i18n.testing)}
>
) : (
- 'Test Connection'
+ intl.formatMessage(i18n.testConnection)
)}
diff --git a/ui/desktop/src/components/settings/tunnel/TunnelSection.tsx b/ui/desktop/src/components/settings/tunnel/TunnelSection.tsx
index 8d29da79..39e93c7a 100644
--- a/ui/desktop/src/components/settings/tunnel/TunnelSection.tsx
+++ b/ui/desktop/src/components/settings/tunnel/TunnelSection.tsx
@@ -16,18 +16,143 @@ import {
import { errorMessage } from '../../../utils/conversionUtils';
import { startTunnel, stopTunnel, getTunnelStatus } from '../../../api/sdk.gen';
import type { TunnelInfo } from '../../../api/types.gen';
+import { defineMessages, useIntl } from '../../../i18n';
-const STATUS_MESSAGES = {
- idle: 'Tunnel is not running',
- starting: 'Starting tunnel...',
- running: 'Tunnel is active',
- error: 'Tunnel encountered an error',
- disabled: 'Tunnel is disabled',
-} as const;
+const i18n = defineMessages({
+ statusIdle: {
+ id: 'tunnelSection.statusIdle',
+ defaultMessage: 'Tunnel is not running',
+ },
+ statusStarting: {
+ id: 'tunnelSection.statusStarting',
+ defaultMessage: 'Starting tunnel...',
+ },
+ statusRunning: {
+ id: 'tunnelSection.statusRunning',
+ defaultMessage: 'Tunnel is active',
+ },
+ statusError: {
+ id: 'tunnelSection.statusError',
+ defaultMessage: 'Tunnel encountered an error',
+ },
+ statusDisabled: {
+ id: 'tunnelSection.statusDisabled',
+ defaultMessage: 'Tunnel is disabled',
+ },
+ mobileApp: {
+ id: 'tunnelSection.mobileApp',
+ defaultMessage: 'Mobile App',
+ },
+ previewFeature: {
+ id: 'tunnelSection.previewFeature',
+ defaultMessage: 'Preview feature:',
+ },
+ previewDescription: {
+ id: 'tunnelSection.previewDescription',
+ defaultMessage: 'Enable remote access to goose from mobile devices using secure tunneling.',
+ },
+ getIosApp: {
+ id: 'tunnelSection.getIosApp',
+ defaultMessage: 'Get the iOS app',
+ },
+ or: {
+ id: 'tunnelSection.or',
+ defaultMessage: 'or',
+ },
+ scanQrCode: {
+ id: 'tunnelSection.scanQrCode',
+ defaultMessage: 'scan QR code',
+ },
+ tunnelStatus: {
+ id: 'tunnelSection.tunnelStatus',
+ defaultMessage: 'Tunnel Status',
+ },
+ starting: {
+ id: 'tunnelSection.starting',
+ defaultMessage: 'Starting...',
+ },
+ showQrCode: {
+ id: 'tunnelSection.showQrCode',
+ defaultMessage: 'Show QR Code',
+ },
+ stopTunnel: {
+ id: 'tunnelSection.stopTunnel',
+ defaultMessage: 'Stop Tunnel',
+ },
+ retry: {
+ id: 'tunnelSection.retry',
+ defaultMessage: 'Retry',
+ },
+ startTunnel: {
+ id: 'tunnelSection.startTunnel',
+ defaultMessage: 'Start Tunnel',
+ },
+ url: {
+ id: 'tunnelSection.url',
+ defaultMessage: 'URL:',
+ },
+ mobileAppConnection: {
+ id: 'tunnelSection.mobileAppConnection',
+ defaultMessage: 'Mobile App Connection',
+ },
+ qrCodeInstructions: {
+ id: 'tunnelSection.qrCodeInstructions',
+ defaultMessage: 'Scan this QR code with the goose mobile app. Do not share this code with anyone else as it is for your personal access.',
+ },
+ connectionDetails: {
+ id: 'tunnelSection.connectionDetails',
+ defaultMessage: 'Connection Details',
+ },
+ tunnelUrl: {
+ id: 'tunnelSection.tunnelUrl',
+ defaultMessage: 'Tunnel URL',
+ },
+ secretKey: {
+ id: 'tunnelSection.secretKey',
+ defaultMessage: 'Secret Key',
+ },
+ close: {
+ id: 'tunnelSection.close',
+ defaultMessage: 'Close',
+ },
+ downloadIosApp: {
+ id: 'tunnelSection.downloadIosApp',
+ defaultMessage: 'Download goose iOS App',
+ },
+ appStoreQrInstructions: {
+ id: 'tunnelSection.appStoreQrInstructions',
+ defaultMessage: 'Scan this QR code with your iPhone camera to install the goose mobile app from the App Store',
+ },
+ openInAppStore: {
+ id: 'tunnelSection.openInAppStore',
+ defaultMessage: 'Open in App Store',
+ },
+ failedToLoadStatus: {
+ id: 'tunnelSection.failedToLoadStatus',
+ defaultMessage: 'Failed to load tunnel status',
+ },
+ failedToStopTunnel: {
+ id: 'tunnelSection.failedToStopTunnel',
+ defaultMessage: 'Failed to stop tunnel',
+ },
+ failedToStartTunnel: {
+ id: 'tunnelSection.failedToStartTunnel',
+ defaultMessage: 'Failed to start tunnel',
+ },
+});
const IOS_APP_STORE_URL = 'https://apps.apple.com/us/app/goose-ai/id6752889295';
+const STATUS_MESSAGE_KEYS = {
+ idle: 'statusIdle',
+ starting: 'statusStarting',
+ running: 'statusRunning',
+ error: 'statusError',
+ disabled: 'statusDisabled',
+} as const;
+
export default function TunnelSection() {
+ const intl = useIntl();
const [tunnelInfo, setTunnelInfo] = useState
({
state: 'idle',
url: '',
@@ -49,14 +174,14 @@ export default function TunnelSection() {
setTunnelInfo(data);
}
} catch (err) {
- const errorMsg = errorMessage(err, 'Failed to load tunnel status');
+ const errorMsg = errorMessage(err, intl.formatMessage(i18n.failedToLoadStatus));
setError(errorMsg);
setTunnelInfo({ state: 'error', url: '', hostname: '', secret: '' });
}
};
loadTunnelInfo();
- }, []);
+ }, [intl]);
const handleToggleTunnel = async () => {
if (tunnelInfo.state === 'running') {
@@ -65,7 +190,7 @@ export default function TunnelSection() {
setTunnelInfo({ state: 'idle', url: '', hostname: '', secret: '' });
setShowQRModal(false);
} catch (err) {
- setError(errorMessage(err, 'Failed to stop tunnel'));
+ setError(errorMessage(err, intl.formatMessage(i18n.failedToStopTunnel)));
try {
const { data } = await getTunnelStatus();
if (data) {
@@ -86,7 +211,7 @@ export default function TunnelSection() {
setShowQRModal(true);
}
} catch (err) {
- const errorMsg = errorMessage(err, 'Failed to start tunnel');
+ const errorMsg = errorMessage(err, intl.formatMessage(i18n.failedToStartTunnel));
setError(errorMsg);
setTunnelInfo({ state: 'error', url: '', hostname: '', secret: '' });
}
@@ -127,28 +252,27 @@ export default function TunnelSection() {
<>
- Mobile App
+ {intl.formatMessage(i18n.mobileApp)}
-
Preview feature: Enable remote access to goose from mobile devices
- using secure tunneling.{' '}
+
{intl.formatMessage(i18n.previewFeature)} {intl.formatMessage(i18n.previewDescription)}{' '}
- Get the iOS app
+ {intl.formatMessage(i18n.getIosApp)}
- {' or '}
+ {' '}{intl.formatMessage(i18n.or)}{' '}
setShowAppStoreQRModal(true)}
className="inline-flex items-center gap-1 underline hover:no-underline"
>
- scan QR code
+ {intl.formatMessage(i18n.scanQrCode)}
@@ -164,29 +288,29 @@ export default function TunnelSection() {
-
Tunnel Status
+
{intl.formatMessage(i18n.tunnelStatus)}
- {STATUS_MESSAGES[tunnelInfo.state]}
+ {intl.formatMessage(i18n[STATUS_MESSAGE_KEYS[tunnelInfo.state]])}
{tunnelInfo.state === 'starting' ? (
- Starting...
+ {intl.formatMessage(i18n.starting)}
) : tunnelInfo.state === 'running' ? (
<>
setShowQRModal(true)} variant="default" size="sm">
- Show QR Code
+ {intl.formatMessage(i18n.showQrCode)}
- Stop Tunnel
+ {intl.formatMessage(i18n.stopTunnel)}
>
) : (
- {tunnelInfo.state === 'error' ? 'Retry' : 'Start Tunnel'}
+ {tunnelInfo.state === 'error' ? intl.formatMessage(i18n.retry) : intl.formatMessage(i18n.startTunnel)}
)}
@@ -195,7 +319,7 @@ export default function TunnelSection() {
{tunnelInfo.state === 'running' && (
- URL: {tunnelInfo.url}
+ {intl.formatMessage(i18n.url)} {tunnelInfo.url}
)}
@@ -205,7 +329,7 @@ export default function TunnelSection() {
- Mobile App Connection
+ {intl.formatMessage(i18n.mobileAppConnection)}
{tunnelInfo.state === 'running' && (
@@ -217,8 +341,7 @@ export default function TunnelSection() {
- Scan this QR code with the goose mobile app. Do not share this code with anyone else
- as it is for your personal access.
+ {intl.formatMessage(i18n.qrCodeInstructions)}
@@ -226,7 +349,7 @@ export default function TunnelSection() {
onClick={() => setShowDetails(!showDetails)}
className="flex items-center justify-between w-full text-sm font-medium hover:opacity-70 transition-opacity"
>
-
Connection Details
+
{intl.formatMessage(i18n.connectionDetails)}
{showDetails ? (
) : (
@@ -237,7 +360,7 @@ export default function TunnelSection() {
{showDetails && (
-
Tunnel URL
+
{intl.formatMessage(i18n.tunnelUrl)}
{tunnelInfo.url}
@@ -254,7 +377,7 @@ export default function TunnelSection() {
-
Secret Key
+
{intl.formatMessage(i18n.secretKey)}
{tunnelInfo.secret}
@@ -283,10 +406,10 @@ export default function TunnelSection() {
setShowQRModal(false)}>
- Close
+ {intl.formatMessage(i18n.close)}
- Stop Tunnel
+ {intl.formatMessage(i18n.stopTunnel)}
@@ -295,7 +418,7 @@ export default function TunnelSection() {
- Download goose iOS App
+ {intl.formatMessage(i18n.downloadIosApp)}
@@ -306,8 +429,7 @@ export default function TunnelSection() {
- Scan this QR code with your iPhone camera to install the goose mobile app from the App
- Store
+ {intl.formatMessage(i18n.appStoreQrInstructions)}
@@ -318,14 +440,14 @@ export default function TunnelSection() {
className="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
- Open in App Store
+ {intl.formatMessage(i18n.openInAppStore)}
setShowAppStoreQRModal(false)}>
- Close
+ {intl.formatMessage(i18n.close)}
diff --git a/ui/desktop/src/components/ui/BackButton.tsx b/ui/desktop/src/components/ui/BackButton.tsx
index d2ce935a..5c0def50 100644
--- a/ui/desktop/src/components/ui/BackButton.tsx
+++ b/ui/desktop/src/components/ui/BackButton.tsx
@@ -4,6 +4,14 @@ import { Button } from './button';
import type { VariantProps } from 'class-variance-authority';
import { buttonVariants } from './button';
import { cn } from '../../utils';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ back: {
+ id: 'backButton.back',
+ defaultMessage: 'Back',
+ },
+});
interface BackButtonProps extends VariantProps
{
onClick?: () => void;
@@ -21,6 +29,7 @@ const BackButton: React.FC = ({
showText = true,
...props
}) => {
+ const intl = useIntl();
const handleExit = useCallback(() => {
if (onClick) {
onClick(); // Custom onClick handler passed via props
@@ -75,7 +84,7 @@ const BackButton: React.FC = ({
{...props}
>
- {showText && 'Back'}
+ {showText && intl.formatMessage(i18n.back)}
);
};
diff --git a/ui/desktop/src/components/ui/ConfirmationModal.tsx b/ui/desktop/src/components/ui/ConfirmationModal.tsx
index dd1fa7f6..a22a3545 100644
--- a/ui/desktop/src/components/ui/ConfirmationModal.tsx
+++ b/ui/desktop/src/components/ui/ConfirmationModal.tsx
@@ -8,6 +8,22 @@ import {
DialogTitle,
} from './dialog';
import { Button } from './button';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ processing: {
+ id: 'confirmationModal.processing',
+ defaultMessage: 'Processing...',
+ },
+ defaultConfirm: {
+ id: 'confirmationModal.defaultConfirm',
+ defaultMessage: 'Yes',
+ },
+ defaultCancel: {
+ id: 'confirmationModal.defaultCancel',
+ defaultMessage: 'No',
+ },
+});
export function ConfirmationModal({
isOpen,
@@ -16,8 +32,8 @@ export function ConfirmationModal({
detail,
onConfirm,
onCancel,
- confirmLabel = 'Yes',
- cancelLabel = 'No',
+ confirmLabel,
+ cancelLabel,
isSubmitting = false,
confirmVariant = 'default',
}: {
@@ -32,6 +48,8 @@ export function ConfirmationModal({
isSubmitting?: boolean; // To handle debounce state
confirmVariant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
}) {
+ const intl = useIntl();
+
return (
!open && onCancel()}>
@@ -51,7 +69,7 @@ export function ConfirmationModal({
disabled={isSubmitting}
className="focus-visible:ring-2 focus-visible:ring-background-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background-default"
>
- {cancelLabel}
+ {cancelLabel || intl.formatMessage(i18n.defaultCancel)}
- {isSubmitting ? 'Processing...' : confirmLabel}
+ {isSubmitting ? intl.formatMessage(i18n.processing) : (confirmLabel || intl.formatMessage(i18n.defaultConfirm))}
diff --git a/ui/desktop/src/components/ui/Diagnostics.tsx b/ui/desktop/src/components/ui/Diagnostics.tsx
index 72bbbf73..f742ce4b 100644
--- a/ui/desktop/src/components/ui/Diagnostics.tsx
+++ b/ui/desktop/src/components/ui/Diagnostics.tsx
@@ -3,6 +3,80 @@ import { AlertTriangle, Download, Github } from 'lucide-react';
import { Button } from './button';
import { toastError } from '../../toasts';
import { diagnostics, systemInfo } from '../../api';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ reportProblem: {
+ id: 'diagnosticsModal.reportProblem',
+ defaultMessage: 'Report a Problem',
+ },
+ description: {
+ id: 'diagnosticsModal.description',
+ defaultMessage:
+ 'You can download a diagnostics zip file to share with the team, or file a bug directly on GitHub with your system details pre-filled. A diagnostics report contains the following:',
+ },
+ systemInfo: {
+ id: 'diagnosticsModal.systemInfo',
+ defaultMessage: 'Basic system info',
+ },
+ sessionMessages: {
+ id: 'diagnosticsModal.sessionMessages',
+ defaultMessage: 'Your current session messages',
+ },
+ logFiles: {
+ id: 'diagnosticsModal.logFiles',
+ defaultMessage: 'Recent log files',
+ },
+ configSettings: {
+ id: 'diagnosticsModal.configSettings',
+ defaultMessage: 'Configuration settings',
+ },
+ sensitiveWarning: {
+ id: 'diagnosticsModal.sensitiveWarning',
+ defaultMessage:
+ 'If your session contains sensitive information, do not share the diagnostics file publicly.',
+ },
+ attachHint: {
+ id: 'diagnosticsModal.attachHint',
+ defaultMessage: 'If you file a bug, consider attaching the diagnostics report to it.',
+ },
+ cancel: {
+ id: 'diagnosticsModal.cancel',
+ defaultMessage: 'Cancel',
+ },
+ downloading: {
+ id: 'diagnosticsModal.downloading',
+ defaultMessage: 'Downloading...',
+ },
+ download: {
+ id: 'diagnosticsModal.download',
+ defaultMessage: 'Download',
+ },
+ opening: {
+ id: 'diagnosticsModal.opening',
+ defaultMessage: 'Opening...',
+ },
+ fileBug: {
+ id: 'diagnosticsModal.fileBug',
+ defaultMessage: 'File Bug on GitHub',
+ },
+ diagnosticsErrorTitle: {
+ id: 'diagnosticsModal.diagnosticsErrorTitle',
+ defaultMessage: 'Diagnostics Error',
+ },
+ diagnosticsErrorMsg: {
+ id: 'diagnosticsModal.diagnosticsErrorMsg',
+ defaultMessage: 'Failed to download diagnostics',
+ },
+ systemInfoErrorTitle: {
+ id: 'diagnosticsModal.systemInfoErrorTitle',
+ defaultMessage: 'Error',
+ },
+ systemInfoErrorMsg: {
+ id: 'diagnosticsModal.systemInfoErrorMsg',
+ defaultMessage: 'Failed to get system information',
+ },
+});
interface DiagnosticsModalProps {
isOpen: boolean;
@@ -15,6 +89,7 @@ export const DiagnosticsModal: React.FC = ({
onClose,
sessionId,
}) => {
+ const intl = useIntl();
const [isDownloading, setIsDownloading] = useState(false);
const [isFilingBug, setIsFilingBug] = useState(false);
@@ -40,8 +115,8 @@ export const DiagnosticsModal: React.FC = ({
onClose();
} catch {
toastError({
- title: 'Diagnostics Error',
- msg: 'Failed to download diagnostics',
+ title: intl.formatMessage(i18n.diagnosticsErrorTitle),
+ msg: intl.formatMessage(i18n.diagnosticsErrorMsg),
});
} finally {
setIsDownloading(false);
@@ -119,8 +194,8 @@ Add any other context about the problem here.
onClose();
} catch {
toastError({
- title: 'Error',
- msg: 'Failed to get system information',
+ title: intl.formatMessage(i18n.systemInfoErrorTitle),
+ msg: intl.formatMessage(i18n.systemInfoErrorMsg),
});
} finally {
setIsFilingBug(false);
@@ -135,24 +210,21 @@ Add any other context about the problem here.
-
Report a Problem
+
{intl.formatMessage(i18n.reportProblem)}
- You can download a diagnostics zip file to share with the team, or file a bug directly
- on GitHub with your system details pre-filled. A diagnostics report contains the
- following:
+ {intl.formatMessage(i18n.description)}
- Basic system info
- Your current session messages
- Recent log files
- Configuration settings
+ {intl.formatMessage(i18n.systemInfo)}
+ {intl.formatMessage(i18n.sessionMessages)}
+ {intl.formatMessage(i18n.logFiles)}
+ {intl.formatMessage(i18n.configSettings)}
- Warning: If your session contains sensitive information, do not share
- the diagnostics file publicly.
+ Warning: {intl.formatMessage(i18n.sensitiveWarning)}
- If you file a bug, consider attaching the diagnostics report to it.
+ {intl.formatMessage(i18n.attachHint)}
@@ -163,7 +235,7 @@ Add any other context about the problem here.
size="sm"
disabled={isDownloading || isFilingBug}
>
- Cancel
+ {intl.formatMessage(i18n.cancel)}
- {isDownloading ? 'Downloading...' : 'Download'}
+ {isDownloading ? intl.formatMessage(i18n.downloading) : intl.formatMessage(i18n.download)}
- {isFilingBug ? 'Opening...' : 'File Bug on GitHub'}
+ {isFilingBug ? intl.formatMessage(i18n.opening) : intl.formatMessage(i18n.fileBug)}
diff --git a/ui/desktop/src/components/ui/JsonSchemaForm.tsx b/ui/desktop/src/components/ui/JsonSchemaForm.tsx
index 21e63bb4..0076d7ee 100644
--- a/ui/desktop/src/components/ui/JsonSchemaForm.tsx
+++ b/ui/desktop/src/components/ui/JsonSchemaForm.tsx
@@ -1,6 +1,46 @@
import React, { useState, useCallback } from 'react';
import { Input } from './input';
import { Button } from './button';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ submit: {
+ id: 'jsonSchemaForm.submit',
+ defaultMessage: 'Submit',
+ },
+ cancel: {
+ id: 'jsonSchemaForm.cancel',
+ defaultMessage: 'Cancel',
+ },
+ fieldRequired: {
+ id: 'jsonSchemaForm.fieldRequired',
+ defaultMessage: 'This field is required',
+ },
+ minLength: {
+ id: 'jsonSchemaForm.minLength',
+ defaultMessage: 'Minimum length is {minLength}',
+ },
+ maxLength: {
+ id: 'jsonSchemaForm.maxLength',
+ defaultMessage: 'Maximum length is {maxLength}',
+ },
+ minValue: {
+ id: 'jsonSchemaForm.minValue',
+ defaultMessage: 'Minimum value is {minimum}',
+ },
+ maxValue: {
+ id: 'jsonSchemaForm.maxValue',
+ defaultMessage: 'Maximum value is {maximum}',
+ },
+ selectPlaceholder: {
+ id: 'jsonSchemaForm.selectPlaceholder',
+ defaultMessage: 'Select...',
+ },
+ noFields: {
+ id: 'jsonSchemaForm.noFields',
+ defaultMessage: 'No fields to display',
+ },
+});
interface JsonSchemaProperty {
type?: string;
@@ -34,10 +74,11 @@ export default function JsonSchemaForm({
schema,
onSubmit,
onCancel,
- submitLabel = 'Submit',
- cancelLabel = 'Cancel',
+ submitLabel,
+ cancelLabel,
disabled = false,
}: JsonSchemaFormProps) {
+ const intl = useIntl();
const [formData, setFormData] = useState
>(() => {
const initial: Record = {};
if (schema.properties) {
@@ -66,32 +107,32 @@ export default function JsonSchemaForm({
const isRequired = schema.required?.includes(key);
if (isRequired && (value === '' || value === null || value === undefined)) {
- return 'This field is required';
+ return intl.formatMessage(i18n.fieldRequired);
}
if (prop.type === 'string' && typeof value === 'string') {
if (!isRequired && value === '') return null;
if (prop.minLength !== undefined && value.length < prop.minLength) {
- return `Minimum length is ${prop.minLength}`;
+ return intl.formatMessage(i18n.minLength, { minLength: prop.minLength });
}
if (prop.maxLength !== undefined && value.length > prop.maxLength) {
- return `Maximum length is ${prop.maxLength}`;
+ return intl.formatMessage(i18n.maxLength, { maxLength: prop.maxLength });
}
}
if ((prop.type === 'number' || prop.type === 'integer') && typeof value === 'number') {
if (prop.minimum !== undefined && value < prop.minimum) {
- return `Minimum value is ${prop.minimum}`;
+ return intl.formatMessage(i18n.minValue, { minimum: prop.minimum });
}
if (prop.maximum !== undefined && value > prop.maximum) {
- return `Maximum value is ${prop.maximum}`;
+ return intl.formatMessage(i18n.maxValue, { maximum: prop.maximum });
}
}
return null;
},
- [schema]
+ [schema, intl]
);
const handleChange = useCallback(
@@ -149,7 +190,7 @@ export default function JsonSchemaForm({
disabled={disabled}
className="flex h-9 w-full rounded-md border focus:border-border-secondary hover:border-border-secondary bg-background-primary px-3 py-1 text-base transition-colors focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
>
- {!isRequired && Select... }
+ {!isRequired && {intl.formatMessage(i18n.selectPlaceholder)} }
{prop.enum.map((option) => (
{option}
@@ -210,7 +251,7 @@ export default function JsonSchemaForm({
};
if (!schema.properties || Object.keys(schema.properties).length === 0) {
- return No fields to display
;
+ return {intl.formatMessage(i18n.noFields)}
;
}
return (
@@ -245,11 +286,11 @@ export default function JsonSchemaForm({
- {submitLabel}
+ {submitLabel ?? intl.formatMessage(i18n.submit)}
{onCancel && (
- {cancelLabel}
+ {cancelLabel ?? intl.formatMessage(i18n.cancel)}
)}
diff --git a/ui/desktop/src/components/ui/RecipeWarningModal.tsx b/ui/desktop/src/components/ui/RecipeWarningModal.tsx
index 3f1cdace..279199d0 100644
--- a/ui/desktop/src/components/ui/RecipeWarningModal.tsx
+++ b/ui/desktop/src/components/ui/RecipeWarningModal.tsx
@@ -11,6 +11,55 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Button } from './button';
import MarkdownContent from '../MarkdownContent';
import { cn } from '../../utils';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ securityWarningTitle: {
+ id: 'recipeWarningModal.securityWarningTitle',
+ defaultMessage: '⚠️ Security Warning',
+ },
+ newRecipeWarningTitle: {
+ id: 'recipeWarningModal.newRecipeWarningTitle',
+ defaultMessage: '⚠️ New Recipe Warning',
+ },
+ firstTimeDescription: {
+ id: 'recipeWarningModal.firstTimeDescription',
+ defaultMessage: "You are about to execute a recipe that you haven't run before. ",
+ },
+ trustSource: {
+ id: 'recipeWarningModal.trustSource',
+ defaultMessage: 'Only proceed if you trust the source of this recipe.',
+ },
+ hiddenCharsWarning: {
+ id: 'recipeWarningModal.hiddenCharsWarning',
+ defaultMessage:
+ 'This recipe contains hidden characters that will be ignored for your safety, as they could be used for malicious purposes.',
+ },
+ recipePreview: {
+ id: 'recipeWarningModal.recipePreview',
+ defaultMessage: 'Recipe Preview:',
+ },
+ titleLabel: {
+ id: 'recipeWarningModal.titleLabel',
+ defaultMessage: 'Title:',
+ },
+ descriptionLabel: {
+ id: 'recipeWarningModal.descriptionLabel',
+ defaultMessage: 'Description:',
+ },
+ instructionsLabel: {
+ id: 'recipeWarningModal.instructionsLabel',
+ defaultMessage: 'Instructions:',
+ },
+ cancel: {
+ id: 'recipeWarningModal.cancel',
+ defaultMessage: 'Cancel',
+ },
+ trustAndExecute: {
+ id: 'recipeWarningModal.trustAndExecute',
+ defaultMessage: 'Trust and Execute',
+ },
+});
interface RecipeWarningModalProps {
isOpen: boolean;
@@ -31,6 +80,8 @@ export function RecipeWarningModal({
recipeDetails,
hasSecurityWarnings = false,
}: RecipeWarningModalProps) {
+ const intl = useIntl();
+
return (
!open && onCancel()}>
@@ -44,12 +95,13 @@ export function RecipeWarningModal({
>
- {hasSecurityWarnings ? '⚠️ Security Warning' : '⚠️ New Recipe Warning'}
+ {hasSecurityWarnings
+ ? intl.formatMessage(i18n.securityWarningTitle)
+ : intl.formatMessage(i18n.newRecipeWarningTitle)}
- {!hasSecurityWarnings &&
- "You are about to execute a recipe that you haven't run before. "}
- Only proceed if you trust the source of this recipe.
+ {!hasSecurityWarnings && intl.formatMessage(i18n.firstTimeDescription)}
+ {intl.formatMessage(i18n.trustSource)}
@@ -59,10 +111,7 @@ export function RecipeWarningModal({
-
- This recipe contains hidden characters that will be ignored for your safety,
- as they could be used for malicious purposes.
-
+
{intl.formatMessage(i18n.hiddenCharsWarning)}
@@ -72,21 +121,26 @@ export function RecipeWarningModal({
-
Recipe Preview:
+
+ {intl.formatMessage(i18n.recipePreview)}
+
{recipeDetails.title && (
- Title: {recipeDetails.title}
+ {intl.formatMessage(i18n.titleLabel)} {recipeDetails.title}
)}
{recipeDetails.description && (
- Description: {recipeDetails.description}
+ {intl.formatMessage(i18n.descriptionLabel)} {' '}
+ {recipeDetails.description}
)}
{recipeDetails.instructions && (
-
Instructions:
+
+ {intl.formatMessage(i18n.instructionsLabel)}
+
)}
@@ -96,9 +150,9 @@ export function RecipeWarningModal({
- Cancel
+ {intl.formatMessage(i18n.cancel)}
- Trust and Execute
+ {intl.formatMessage(i18n.trustAndExecute)}
diff --git a/ui/desktop/src/components/ui/dialog.tsx b/ui/desktop/src/components/ui/dialog.tsx
index b65fd40c..9f3b74dd 100644
--- a/ui/desktop/src/components/ui/dialog.tsx
+++ b/ui/desktop/src/components/ui/dialog.tsx
@@ -5,6 +5,14 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
import { XIcon } from 'lucide-react';
import { cn } from '../../utils';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ close: {
+ id: 'dialog.close',
+ defaultMessage: 'Close',
+ },
+});
function Dialog({ ...props }: React.ComponentProps
) {
return ;
@@ -43,6 +51,7 @@ function DialogContent({
children,
...props
}: React.ComponentProps) {
+ const intl = useIntl();
return (
@@ -57,7 +66,7 @@ function DialogContent({
{children}
- Close
+ {intl.formatMessage(i18n.close)}
diff --git a/ui/desktop/src/components/ui/sheet.tsx b/ui/desktop/src/components/ui/sheet.tsx
index f9b4ef13..ad2e3147 100644
--- a/ui/desktop/src/components/ui/sheet.tsx
+++ b/ui/desktop/src/components/ui/sheet.tsx
@@ -5,6 +5,14 @@ import * as SheetPrimitive from '@radix-ui/react-dialog';
import { XIcon } from 'lucide-react';
import { cn } from '../../utils';
+import { defineMessages, useIntl } from '../../i18n';
+
+const i18n = defineMessages({
+ close: {
+ id: 'sheet.close',
+ defaultMessage: 'Close',
+ },
+});
function Sheet({ ...props }: React.ComponentProps) {
return ;
@@ -46,6 +54,7 @@ function SheetContent({
}: React.ComponentProps & {
side?: 'top' | 'right' | 'bottom' | 'left';
}) {
+ const intl = useIntl();
return (
@@ -68,7 +77,7 @@ function SheetContent({
{children}
- Close
+ {intl.formatMessage(i18n.close)}
diff --git a/ui/desktop/src/i18n/i18n.test.ts b/ui/desktop/src/i18n/i18n.test.ts
new file mode 100644
index 00000000..16906823
--- /dev/null
+++ b/ui/desktop/src/i18n/i18n.test.ts
@@ -0,0 +1,80 @@
+import { describe, it, expect, vi, afterEach } from 'vitest';
+import { getLocale } from './index';
+
+// Helper to mock window.appConfig for tests
+function mockAppConfig(values: Record) {
+ (window as unknown as Record).appConfig = {
+ get: (key: string) => values[key],
+ getAll: () => values,
+ };
+}
+
+describe('getLocale', () => {
+ afterEach(() => {
+ // Clean up appConfig mock
+ if (typeof window !== 'undefined') {
+ delete (window as unknown as Record).appConfig;
+ }
+ vi.restoreAllMocks();
+ });
+
+ it('returns "en" as the default fallback', () => {
+ // navigator.language returns something unsupported
+ vi.stubGlobal('navigator', { language: 'xx-XX' });
+ expect(getLocale()).toEqual({ locale: 'en', messageLocale: 'en' });
+ });
+
+ it('preserves regional tag for formatting when base language is supported', () => {
+ vi.stubGlobal('navigator', { language: 'en-US' });
+ expect(getLocale()).toEqual({ locale: 'en-US', messageLocale: 'en' });
+ });
+
+ it('returns exact match when navigator.language matches a supported locale', () => {
+ vi.stubGlobal('navigator', { language: 'en' });
+ expect(getLocale()).toEqual({ locale: 'en', messageLocale: 'en' });
+ });
+
+ it('respects GOOSE_LOCALE over navigator.language', () => {
+ mockAppConfig({ GOOSE_LOCALE: 'en' });
+ vi.stubGlobal('navigator', { language: 'xx-XX' });
+ expect(getLocale()).toEqual({ locale: 'en', messageLocale: 'en' });
+ });
+
+ it('preserves regional tag from GOOSE_LOCALE', () => {
+ mockAppConfig({ GOOSE_LOCALE: 'en-GB' });
+ vi.stubGlobal('navigator', { language: 'xx-XX' });
+ expect(getLocale()).toEqual({ locale: 'en-GB', messageLocale: 'en' });
+ });
+
+ it('falls back to base language tag for message catalog', () => {
+ // "en-GB" should use "en" catalog but keep "en-GB" for formatting
+ vi.stubGlobal('navigator', { language: 'en-GB' });
+ expect(getLocale()).toEqual({ locale: 'en-GB', messageLocale: 'en' });
+ });
+
+ it('falls back to base language when locale tag is invalid BCP 47', () => {
+ // "en-" is not a valid BCP 47 tag and would cause RangeError in Intl APIs
+ mockAppConfig({ GOOSE_LOCALE: 'en-' });
+ vi.stubGlobal('navigator', { language: 'xx-XX' });
+ expect(getLocale()).toEqual({ locale: 'en', messageLocale: 'en' });
+ });
+});
+
+describe('loadMessages', () => {
+ it('returns empty object for English locale', async () => {
+ const { loadMessages } = await import('./index');
+ const messages = await loadMessages('en');
+ expect(messages).toEqual({});
+ });
+
+ it('returns empty object for unsupported locale (with warning)', async () => {
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const { loadMessages } = await import('./index');
+ const messages = await loadMessages('xx');
+ expect(messages).toEqual({});
+ expect(warnSpy).toHaveBeenCalledWith(
+ expect.stringContaining('No message catalog found')
+ );
+ warnSpy.mockRestore();
+ });
+});
diff --git a/ui/desktop/src/i18n/index.ts b/ui/desktop/src/i18n/index.ts
new file mode 100644
index 00000000..aa89bc2c
--- /dev/null
+++ b/ui/desktop/src/i18n/index.ts
@@ -0,0 +1,89 @@
+/**
+ * Locale detection and message loading for the i18n system.
+ *
+ * Locale resolution order:
+ * 1. GOOSE_LOCALE config value (set via environment variable, passed through appConfig)
+ * 2. navigator.language (browser/OS locale)
+ * 3. "en" (fallback)
+ */
+
+// Re-export react-intl utilities that components use directly
+export { defineMessages, useIntl } from 'react-intl';
+
+/** The set of locales that have translation catalogs. */
+const SUPPORTED_LOCALES = new Set(['en']);
+
+/**
+ * Detect the user's preferred locale.
+ *
+ * Returns two values:
+ * - `locale`: the full BCP 47 tag (e.g. "en-GB") for formatting (dates, numbers).
+ * - `messageLocale`: the base language that has a translation catalog (e.g. "en").
+ */
+export function getLocale(): { locale: string; messageLocale: string } {
+ const explicit =
+ typeof window !== 'undefined' && window.appConfig
+ ? window.appConfig.get('GOOSE_LOCALE')
+ : undefined;
+
+ const candidates: string[] = [];
+
+ if (typeof explicit === 'string' && explicit) {
+ candidates.push(explicit);
+ }
+
+ if (typeof navigator !== 'undefined' && navigator.language) {
+ candidates.push(navigator.language);
+ }
+
+ for (const tag of candidates) {
+ // Exact match first
+ if (SUPPORTED_LOCALES.has(tag)) return { locale: tag, messageLocale: tag };
+ // Try base language (e.g. "pt-BR" → "pt") for the catalog, but keep the
+ // full regional tag for formatting so date/number output respects the region.
+ const base = tag.split('-')[0];
+ if (SUPPORTED_LOCALES.has(base)) {
+ // Validate the full tag is a well-formed BCP 47 locale before using it
+ // for formatting. Invalid tags (e.g. "en-") would cause RangeError in
+ // Intl APIs, so fall back to the base language in that case.
+ let locale = base;
+ try {
+ [locale] = Intl.getCanonicalLocales(tag);
+ } catch {
+ // tag is not valid BCP 47 — use the base language instead
+ }
+ return { locale, messageLocale: base };
+ }
+ }
+
+ return { locale: 'en', messageLocale: 'en' };
+}
+
+/** Resolved locales — computed once at module load. */
+const resolvedLocale = getLocale();
+/** Full BCP 47 tag for date/number formatting (e.g. "en-GB"). */
+export const currentLocale = resolvedLocale.locale;
+/** Base language for loading message catalogs (e.g. "en"). */
+export const currentMessageLocale = resolvedLocale.messageLocale;
+
+/**
+ * Load compiled messages for a given locale.
+ * Returns an empty object for English (react-intl uses defaultMessage as fallback).
+ */
+export async function loadMessages(
+ locale: string
+): Promise> {
+ if (locale === 'en') {
+ // English strings live in source code as defaultMessage — no catalog needed.
+ return {};
+ }
+
+ try {
+ // Dynamic import so compiled translation bundles are code-split.
+ const mod = await import(`./compiled/${locale}.json`);
+ return mod.default ?? mod;
+ } catch {
+ console.warn(`[i18n] No message catalog found for locale "${locale}", falling back to English.`);
+ return {};
+ }
+}
diff --git a/ui/desktop/src/i18n/messages/en.json b/ui/desktop/src/i18n/messages/en.json
new file mode 100644
index 00000000..93419505
--- /dev/null
+++ b/ui/desktop/src/i18n/messages/en.json
@@ -0,0 +1,4670 @@
+{
+ "alertBox.autoCompactAt": {
+ "defaultMessage": "Auto compact at"
+ },
+ "alertBox.compactNow": {
+ "defaultMessage": "Compact now"
+ },
+ "alertBox.failedToSaveThreshold": {
+ "defaultMessage": "Failed to save threshold: {error}"
+ },
+ "announcementModal.gotIt": {
+ "defaultMessage": "Got it!"
+ },
+ "appLayout.closeNavigation": {
+ "defaultMessage": "Close navigation"
+ },
+ "appLayout.openNavigation": {
+ "defaultMessage": "Open navigation"
+ },
+ "appsView.customApp": {
+ "defaultMessage": "Custom app"
+ },
+ "appsView.description": {
+ "defaultMessage": "Applications from your MCP servers and Apps build by goose itself. You can ask it to create new apps through the chat interface and they will appear here."
+ },
+ "appsView.errorLoading": {
+ "defaultMessage": "Error loading apps: {error}"
+ },
+ "appsView.importApp": {
+ "defaultMessage": "Import App"
+ },
+ "appsView.launch": {
+ "defaultMessage": "Launch"
+ },
+ "appsView.loading": {
+ "defaultMessage": "Loading apps..."
+ },
+ "appsView.noAppsDescription": {
+ "defaultMessage": "Open a chat and ask goose for the app you want to have. It can build one for you and that will appear here. Or if somebody shared an app, you can import it using the button above."
+ },
+ "appsView.noAppsTitle": {
+ "defaultMessage": "No apps available"
+ },
+ "appsView.retry": {
+ "defaultMessage": "Retry"
+ },
+ "appsView.title": {
+ "defaultMessage": "Apps"
+ },
+ "backButton.back": {
+ "defaultMessage": "Back"
+ },
+ "baseChat.failedToLoadSession": {
+ "defaultMessage": "Failed to Load Session"
+ },
+ "baseChat.goHome": {
+ "defaultMessage": "Go home"
+ },
+ "baseChat.noSession": {
+ "defaultMessage": "No Session"
+ },
+ "baseChat.recipeCreatedMessage": {
+ "defaultMessage": "\"{title}\" has been saved and is ready to use."
+ },
+ "baseChat.recipeCreatedTitle": {
+ "defaultMessage": "Recipe created successfully!"
+ },
+ "bottomMenuExtensionSelection.extensionToggleError": {
+ "defaultMessage": "Extension Toggle Error"
+ },
+ "bottomMenuExtensionSelection.extensionUpdated": {
+ "defaultMessage": "Extension Updated"
+ },
+ "bottomMenuExtensionSelection.extensionWillBeDisabled": {
+ "defaultMessage": "{name} will be disabled in new chats"
+ },
+ "bottomMenuExtensionSelection.extensionWillBeEnabled": {
+ "defaultMessage": "{name} will be enabled in new chats"
+ },
+ "bottomMenuExtensionSelection.extensionsForNewChats": {
+ "defaultMessage": "Extensions for new chats"
+ },
+ "bottomMenuExtensionSelection.extensionsForThisSession": {
+ "defaultMessage": "Extensions for this chat session"
+ },
+ "bottomMenuExtensionSelection.manageExtensions": {
+ "defaultMessage": "manage extensions"
+ },
+ "bottomMenuExtensionSelection.noActiveSession": {
+ "defaultMessage": "No active session found. Please start a chat session first."
+ },
+ "bottomMenuExtensionSelection.noExtensionsAvailable": {
+ "defaultMessage": "no extensions available"
+ },
+ "bottomMenuExtensionSelection.noExtensionsFound": {
+ "defaultMessage": "no extensions found"
+ },
+ "bottomMenuExtensionSelection.searchExtensions": {
+ "defaultMessage": "search extensions..."
+ },
+ "bottomMenuModeSelection.autoFallback": {
+ "defaultMessage": "auto"
+ },
+ "bottomMenuModeSelection.automaticModeDescription": {
+ "defaultMessage": "Automatic mode selection"
+ },
+ "bottomMenuModeSelection.currentModeTitle": {
+ "defaultMessage": "Current mode: {label} - {description}"
+ },
+ "cardButtons.configure": {
+ "defaultMessage": "Configure"
+ },
+ "cardButtons.launch": {
+ "defaultMessage": "Launch"
+ },
+ "chatInput.contextWindow": {
+ "defaultMessage": "Context window"
+ },
+ "chatInput.createRecipeFromSession": {
+ "defaultMessage": "Create Recipe from Session"
+ },
+ "chatInput.dictationError": {
+ "defaultMessage": "Dictation Error"
+ },
+ "chatInput.failedToReadImage": {
+ "defaultMessage": "Failed to read image file"
+ },
+ "chatInput.processingDroppedFiles": {
+ "defaultMessage": "Processing dropped files..."
+ },
+ "chatInput.recording": {
+ "defaultMessage": "Recording..."
+ },
+ "chatInput.removeFile": {
+ "defaultMessage": "Remove file"
+ },
+ "chatInput.removeImage": {
+ "defaultMessage": "Remove image"
+ },
+ "chatInput.restartingSession": {
+ "defaultMessage": "Restarting session..."
+ },
+ "chatInput.send": {
+ "defaultMessage": "Send"
+ },
+ "chatInput.tooManyTools": {
+ "defaultMessage": "Too many tools can degrade performance. Tool count: {toolCount} (recommend: {recommended})"
+ },
+ "chatInput.transcribing": {
+ "defaultMessage": "Transcribing..."
+ },
+ "chatInput.typeMessage": {
+ "defaultMessage": "Type a message to send"
+ },
+ "chatInput.unknownType": {
+ "defaultMessage": "Unknown type"
+ },
+ "chatInput.viewEditRecipe": {
+ "defaultMessage": "View/Edit Recipe"
+ },
+ "chatInput.viewExtensions": {
+ "defaultMessage": "View extensions"
+ },
+ "chatInput.waitingForImages": {
+ "defaultMessage": "Waiting for images to save..."
+ },
+ "chatSessionsDropdown.newChat": {
+ "defaultMessage": "New Chat"
+ },
+ "chatSessionsDropdown.showAll": {
+ "defaultMessage": "Show All"
+ },
+ "chatSettings.modeDescription": {
+ "defaultMessage": "Configure how Goose interacts with tools and extensions"
+ },
+ "chatSettings.modeTitle": {
+ "defaultMessage": "Mode"
+ },
+ "chatSettings.responseStylesDescription": {
+ "defaultMessage": "Choose how Goose should format and style its responses"
+ },
+ "chatSettings.responseStylesTitle": {
+ "defaultMessage": "Response Styles"
+ },
+ "condensedRenderer.newChat": {
+ "defaultMessage": "New Chat"
+ },
+ "configSettings.configReset": {
+ "defaultMessage": "Configuration Reset"
+ },
+ "configSettings.configResetMsg": {
+ "defaultMessage": "All changes have been reverted"
+ },
+ "configSettings.configUpdated": {
+ "defaultMessage": "Configuration Updated"
+ },
+ "configSettings.configUpdatedMsg": {
+ "defaultMessage": "Successfully saved \"{name}\""
+ },
+ "configSettings.configurationEditor": {
+ "defaultMessage": "Configuration Editor"
+ },
+ "configSettings.description": {
+ "defaultMessage": "Edit your goose configuration settings"
+ },
+ "configSettings.descriptionWithProvider": {
+ "defaultMessage": "Edit your goose configuration settings (current settings for {provider})"
+ },
+ "configSettings.done": {
+ "defaultMessage": "Done"
+ },
+ "configSettings.editConfiguration": {
+ "defaultMessage": "Edit Configuration"
+ },
+ "configSettings.enterValue": {
+ "defaultMessage": "Enter {name}"
+ },
+ "configSettings.noSettings": {
+ "defaultMessage": "No configuration settings found."
+ },
+ "configSettings.resetChanges": {
+ "defaultMessage": "Reset Changes"
+ },
+ "configSettings.saveFailed": {
+ "defaultMessage": "Save Failed"
+ },
+ "configSettings.saveFailedMsg": {
+ "defaultMessage": "Failed to save \"{name}\""
+ },
+ "configSettings.saving": {
+ "defaultMessage": "Saving..."
+ },
+ "configSettings.title": {
+ "defaultMessage": "Configuration"
+ },
+ "configureApproveMode.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "configureApproveMode.description": {
+ "defaultMessage": "Approve requests can either be given to all tool requests or determine which actions may need integration"
+ },
+ "configureApproveMode.manualApproval": {
+ "defaultMessage": "Manual approval"
+ },
+ "configureApproveMode.manualApprovalDescription": {
+ "defaultMessage": "All tools, extensions and file modifications will require human approval"
+ },
+ "configureApproveMode.save": {
+ "defaultMessage": "Save"
+ },
+ "configureApproveMode.saving": {
+ "defaultMessage": "Saving..."
+ },
+ "configureApproveMode.smartApproval": {
+ "defaultMessage": "Smart approval"
+ },
+ "configureApproveMode.smartApprovalDescription": {
+ "defaultMessage": "Intelligently determine which actions need approval based on risk level"
+ },
+ "configureApproveMode.title": {
+ "defaultMessage": "Configure approve mode"
+ },
+ "confirmationModal.defaultCancel": {
+ "defaultMessage": "No"
+ },
+ "confirmationModal.defaultConfirm": {
+ "defaultMessage": "Yes"
+ },
+ "confirmationModal.processing": {
+ "defaultMessage": "Processing..."
+ },
+ "conversationLimitsDropdown.conversationLimits": {
+ "defaultMessage": "Conversation Limits"
+ },
+ "conversationLimitsDropdown.maxTurns": {
+ "defaultMessage": "Max Turns"
+ },
+ "conversationLimitsDropdown.maxTurnsDescription": {
+ "defaultMessage": "Maximum agent turns before Goose asks for user input"
+ },
+ "costTracker.costUnavailable": {
+ "defaultMessage": "Cost data not available for {model} ({inputTokens} input, {outputTokens} output tokens)"
+ },
+ "costTracker.inputOutputTooltip": {
+ "defaultMessage": "Input: {inputTokens} tokens ({inputCost}) | Output: {outputTokens} tokens ({outputCost})"
+ },
+ "costTracker.pricingUnavailable": {
+ "defaultMessage": "Pricing data unavailable for {model}"
+ },
+ "costTracker.sessionCostBreakdown": {
+ "defaultMessage": "Session cost breakdown:"
+ },
+ "costTracker.totalSessionCost": {
+ "defaultMessage": "Total session cost: {cost}"
+ },
+ "createEditRecipe.clickToGenerateDeeplink": {
+ "defaultMessage": "Click to generate deeplink"
+ },
+ "createEditRecipe.close": {
+ "defaultMessage": "Close"
+ },
+ "createEditRecipe.copied": {
+ "defaultMessage": "Copied!"
+ },
+ "createEditRecipe.copy": {
+ "defaultMessage": "Copy"
+ },
+ "createEditRecipe.copyLinkDescription": {
+ "defaultMessage": "Copy this link to share with friends or paste directly in Chrome to open"
+ },
+ "createEditRecipe.createRecipeTitle": {
+ "defaultMessage": "Create Recipe"
+ },
+ "createEditRecipe.createSubtitle": {
+ "defaultMessage": "Create a new recipe to define agent behavior and capabilities for reusable chat sessions."
+ },
+ "createEditRecipe.editSubtitle": {
+ "defaultMessage": "You can edit the recipe below to change the agent's behavior in a new session."
+ },
+ "createEditRecipe.generatingDeeplink": {
+ "defaultMessage": "Generating deeplink..."
+ },
+ "createEditRecipe.learnMore": {
+ "defaultMessage": "Learn more"
+ },
+ "createEditRecipe.recipeSavedAndLaunchedMsg": {
+ "defaultMessage": "Recipe saved and launched successfully"
+ },
+ "createEditRecipe.recipeSavedMsg": {
+ "defaultMessage": "Recipe saved successfully"
+ },
+ "createEditRecipe.saveAndRunFailed": {
+ "defaultMessage": "Save and Run Failed"
+ },
+ "createEditRecipe.saveAndRunFailedMsg": {
+ "defaultMessage": "Failed to save and run recipe: {error}"
+ },
+ "createEditRecipe.saveAndRunRecipe": {
+ "defaultMessage": "Save & Run Recipe"
+ },
+ "createEditRecipe.saveFailed": {
+ "defaultMessage": "Save Failed"
+ },
+ "createEditRecipe.saveFailedMsg": {
+ "defaultMessage": "Failed to save recipe: {error}"
+ },
+ "createEditRecipe.saveRecipe": {
+ "defaultMessage": "Save Recipe"
+ },
+ "createEditRecipe.saving": {
+ "defaultMessage": "Saving..."
+ },
+ "createEditRecipe.validationFailed": {
+ "defaultMessage": "Validation Failed"
+ },
+ "createEditRecipe.validationMsg": {
+ "defaultMessage": "Please fill in all required fields and ensure JSON schema is valid."
+ },
+ "createEditRecipe.viewEditRecipeTitle": {
+ "defaultMessage": "View/edit recipe"
+ },
+ "createRecipeFromSession.analyzingTitle": {
+ "defaultMessage": "Analyzing your conversation"
+ },
+ "createRecipeFromSession.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "createRecipeFromSession.createAndRunRecipe": {
+ "defaultMessage": "Create & Run Recipe"
+ },
+ "createRecipeFromSession.createRecipe": {
+ "defaultMessage": "Create Recipe"
+ },
+ "createRecipeFromSession.creating": {
+ "defaultMessage": "Creating..."
+ },
+ "createRecipeFromSession.extractingInsights": {
+ "defaultMessage": "Extracting insights from your chat"
+ },
+ "createRecipeFromSession.failedToCreateDefaultMsg": {
+ "defaultMessage": "An unexpected error occurred while creating the recipe. Please try again."
+ },
+ "createRecipeFromSession.failedToCreateTitle": {
+ "defaultMessage": "Failed to create recipe"
+ },
+ "createRecipeFromSession.stageComplete": {
+ "defaultMessage": "Complete!"
+ },
+ "createRecipeFromSession.stageExtracting": {
+ "defaultMessage": "Extracting main topics..."
+ },
+ "createRecipeFromSession.stageFinalizing": {
+ "defaultMessage": "Finalizing details..."
+ },
+ "createRecipeFromSession.stageGenerating": {
+ "defaultMessage": "Generating recipe structure..."
+ },
+ "createRecipeFromSession.stageIdentifying": {
+ "defaultMessage": "Identifying key patterns..."
+ },
+ "createRecipeFromSession.stageReading": {
+ "defaultMessage": "Reading your conversation..."
+ },
+ "createRecipeFromSession.subtitle": {
+ "defaultMessage": "Create a reusable recipe based on your current conversation."
+ },
+ "createRecipeFromSession.title": {
+ "defaultMessage": "Create Recipe from Session"
+ },
+ "createSubRecipeInline.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "createSubRecipeInline.closeModal": {
+ "defaultMessage": "Close create subrecipe modal"
+ },
+ "createSubRecipeInline.createAndAdd": {
+ "defaultMessage": "Create & Add Subrecipe"
+ },
+ "createSubRecipeInline.createdSuccess": {
+ "defaultMessage": "Subrecipe created successfully"
+ },
+ "createSubRecipeInline.creating": {
+ "defaultMessage": "Creating..."
+ },
+ "createSubRecipeInline.duplicateName": {
+ "defaultMessage": "Duplicate Name"
+ },
+ "createSubRecipeInline.duplicateNameMsg": {
+ "defaultMessage": "A subrecipe named \"{name}\" already exists. Please use a unique name."
+ },
+ "createSubRecipeInline.instructionsLabel": {
+ "defaultMessage": "Instructions"
+ },
+ "createSubRecipeInline.instructionsPlaceholder": {
+ "defaultMessage": "Instructions for the AI when this subrecipe is called..."
+ },
+ "createSubRecipeInline.nameHint": {
+ "defaultMessage": "Unique identifier used to generate the tool name"
+ },
+ "createSubRecipeInline.nameLabel": {
+ "defaultMessage": "Name"
+ },
+ "createSubRecipeInline.namePlaceholder": {
+ "defaultMessage": "e.g., security_scan"
+ },
+ "createSubRecipeInline.preconfiguredValues": {
+ "defaultMessage": "Pre-configured Values"
+ },
+ "createSubRecipeInline.preconfiguredValuesHint": {
+ "defaultMessage": "Optional parameter values that are always passed to the subrecipe"
+ },
+ "createSubRecipeInline.recipeDescriptionLabel": {
+ "defaultMessage": "Recipe Description"
+ },
+ "createSubRecipeInline.recipeDescriptionPlaceholder": {
+ "defaultMessage": "What this recipe does when executed"
+ },
+ "createSubRecipeInline.recipeTitleLabel": {
+ "defaultMessage": "Recipe Title"
+ },
+ "createSubRecipeInline.recipeTitlePlaceholder": {
+ "defaultMessage": "e.g., Security Analysis Tool"
+ },
+ "createSubRecipeInline.saveFailed": {
+ "defaultMessage": "Save Failed"
+ },
+ "createSubRecipeInline.saveFailedMsg": {
+ "defaultMessage": "Failed to save subrecipe: {error}"
+ },
+ "createSubRecipeInline.sequentialHint": {
+ "defaultMessage": "(Forces sequential execution of multiple instances)"
+ },
+ "createSubRecipeInline.sequentialLabel": {
+ "defaultMessage": "Sequential when repeated"
+ },
+ "createSubRecipeInline.subtitle": {
+ "defaultMessage": "Create a simple recipe to use as a callable tool in your main recipe"
+ },
+ "createSubRecipeInline.title": {
+ "defaultMessage": "Create New Subrecipe"
+ },
+ "createSubRecipeInline.toolDescriptionLabel": {
+ "defaultMessage": "Tool Description"
+ },
+ "createSubRecipeInline.toolDescriptionPlaceholder": {
+ "defaultMessage": "Optional description shown when this is called as a tool"
+ },
+ "createSubRecipeInline.validationFailed": {
+ "defaultMessage": "Validation Failed"
+ },
+ "createSubRecipeInline.validationMsg": {
+ "defaultMessage": "Name, title, recipe description, and instructions are required."
+ },
+ "creditsExhaustedNotification.addCredits": {
+ "defaultMessage": "Add credits"
+ },
+ "creditsExhaustedNotification.insufficientCredits": {
+ "defaultMessage": "Insufficient Credits"
+ },
+ "cronPicker.april": {
+ "defaultMessage": "April"
+ },
+ "cronPicker.at": {
+ "defaultMessage": "at"
+ },
+ "cronPicker.atMinute": {
+ "defaultMessage": "at minute"
+ },
+ "cronPicker.atSecond": {
+ "defaultMessage": "at second"
+ },
+ "cronPicker.august": {
+ "defaultMessage": "August"
+ },
+ "cronPicker.day": {
+ "defaultMessage": "Day"
+ },
+ "cronPicker.december": {
+ "defaultMessage": "December"
+ },
+ "cronPicker.every": {
+ "defaultMessage": "Every"
+ },
+ "cronPicker.february": {
+ "defaultMessage": "February"
+ },
+ "cronPicker.friday": {
+ "defaultMessage": "Friday"
+ },
+ "cronPicker.hour": {
+ "defaultMessage": "Hour"
+ },
+ "cronPicker.inMonth": {
+ "defaultMessage": "in"
+ },
+ "cronPicker.january": {
+ "defaultMessage": "January"
+ },
+ "cronPicker.july": {
+ "defaultMessage": "July"
+ },
+ "cronPicker.june": {
+ "defaultMessage": "June"
+ },
+ "cronPicker.march": {
+ "defaultMessage": "March"
+ },
+ "cronPicker.may": {
+ "defaultMessage": "May"
+ },
+ "cronPicker.minute": {
+ "defaultMessage": "Minute"
+ },
+ "cronPicker.monday": {
+ "defaultMessage": "Monday"
+ },
+ "cronPicker.month": {
+ "defaultMessage": "Month"
+ },
+ "cronPicker.november": {
+ "defaultMessage": "November"
+ },
+ "cronPicker.october": {
+ "defaultMessage": "October"
+ },
+ "cronPicker.on": {
+ "defaultMessage": "on"
+ },
+ "cronPicker.onDay": {
+ "defaultMessage": "on day"
+ },
+ "cronPicker.saturday": {
+ "defaultMessage": "Saturday"
+ },
+ "cronPicker.september": {
+ "defaultMessage": "September"
+ },
+ "cronPicker.sunday": {
+ "defaultMessage": "Sunday"
+ },
+ "cronPicker.thursday": {
+ "defaultMessage": "Thursday"
+ },
+ "cronPicker.tuesday": {
+ "defaultMessage": "Tuesday"
+ },
+ "cronPicker.wednesday": {
+ "defaultMessage": "Wednesday"
+ },
+ "cronPicker.week": {
+ "defaultMessage": "Week"
+ },
+ "cronPicker.year": {
+ "defaultMessage": "Year"
+ },
+ "customProviderForm.add": {
+ "defaultMessage": "Add"
+ },
+ "customProviderForm.anthropicCompatible": {
+ "defaultMessage": "Anthropic Compatible"
+ },
+ "customProviderForm.apiBasePath": {
+ "defaultMessage": "API Base Path (optional)"
+ },
+ "customProviderForm.apiBasePathHint": {
+ "defaultMessage": "Override the default API path. Leave blank to use the provider's default path."
+ },
+ "customProviderForm.apiBasePathPlaceholder": {
+ "defaultMessage": "e.g., v1/chat/completions or project_id/v1"
+ },
+ "customProviderForm.apiKey": {
+ "defaultMessage": "API Key"
+ },
+ "customProviderForm.apiKeyPlaceholderExisting": {
+ "defaultMessage": "Leave blank to keep existing key"
+ },
+ "customProviderForm.apiKeyPlaceholderNew": {
+ "defaultMessage": "Your API key"
+ },
+ "customProviderForm.apiKeyRequired": {
+ "defaultMessage": "API key is required"
+ },
+ "customProviderForm.apiUrl": {
+ "defaultMessage": "API URL"
+ },
+ "customProviderForm.apiUrlPlaceholder": {
+ "defaultMessage": "https://api.example.com"
+ },
+ "customProviderForm.apiUrlRequired": {
+ "defaultMessage": "API URL is required"
+ },
+ "customProviderForm.attachments": {
+ "defaultMessage": "Attachments"
+ },
+ "customProviderForm.authHint": {
+ "defaultMessage": "Local LLMs like Ollama typically don't require an API key."
+ },
+ "customProviderForm.authentication": {
+ "defaultMessage": "Authentication"
+ },
+ "customProviderForm.availableModels": {
+ "defaultMessage": "Available Models (comma-separated)"
+ },
+ "customProviderForm.back": {
+ "defaultMessage": "← Back"
+ },
+ "customProviderForm.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "customProviderForm.cannotDeleteActive": {
+ "defaultMessage": "You cannot delete this provider while it's currently in use. Please switch to a different model first."
+ },
+ "customProviderForm.chooseSetup": {
+ "defaultMessage": "Choose how you'd like to set up your provider."
+ },
+ "customProviderForm.clear": {
+ "defaultMessage": "Clear"
+ },
+ "customProviderForm.configureManually": {
+ "defaultMessage": "Configure manually"
+ },
+ "customProviderForm.configureManuallyDesc": {
+ "defaultMessage": "Enter all provider details yourself"
+ },
+ "customProviderForm.confirmDelete": {
+ "defaultMessage": "Confirm Delete"
+ },
+ "customProviderForm.createProvider": {
+ "defaultMessage": "Create Provider"
+ },
+ "customProviderForm.customHeaders": {
+ "defaultMessage": "Custom Headers"
+ },
+ "customProviderForm.customHeadersHint": {
+ "defaultMessage": "Add custom HTTP headers to include in requests to the provider. Click the \"+\" button to add after filling both fields."
+ },
+ "customProviderForm.deleteConfirmation": {
+ "defaultMessage": "Are you sure you want to delete this custom provider? This will permanently remove the provider and its stored API key. This action cannot be undone."
+ },
+ "customProviderForm.deleteProvider": {
+ "defaultMessage": "Delete Provider"
+ },
+ "customProviderForm.displayName": {
+ "defaultMessage": "Display Name"
+ },
+ "customProviderForm.displayNamePlaceholder": {
+ "defaultMessage": "Your Provider Name"
+ },
+ "customProviderForm.displayNameRequired": {
+ "defaultMessage": "Display name is required"
+ },
+ "customProviderForm.docs": {
+ "defaultMessage": "Docs"
+ },
+ "customProviderForm.headerBothRequired": {
+ "defaultMessage": "Both header name and value must be entered"
+ },
+ "customProviderForm.headerDuplicate": {
+ "defaultMessage": "A header with this name already exists"
+ },
+ "customProviderForm.headerNamePlaceholder": {
+ "defaultMessage": "Header name"
+ },
+ "customProviderForm.headerNoSpaces": {
+ "defaultMessage": "Header name cannot contain spaces"
+ },
+ "customProviderForm.modelsPlaceholder": {
+ "defaultMessage": "model-a, model-b, model-c"
+ },
+ "customProviderForm.modelsRequired": {
+ "defaultMessage": "At least one model is required"
+ },
+ "customProviderForm.ollamaCompatible": {
+ "defaultMessage": "Ollama Compatible"
+ },
+ "customProviderForm.openaiCompatible": {
+ "defaultMessage": "OpenAI Compatible"
+ },
+ "customProviderForm.providerType": {
+ "defaultMessage": "Provider Type"
+ },
+ "customProviderForm.reasoning": {
+ "defaultMessage": "Reasoning"
+ },
+ "customProviderForm.requiresApiKey": {
+ "defaultMessage": "This provider requires an API key"
+ },
+ "customProviderForm.startFromTemplate": {
+ "defaultMessage": "Start from a provider template"
+ },
+ "customProviderForm.startFromTemplateDesc": {
+ "defaultMessage": "Pick a known provider and we'll auto-fill the configuration"
+ },
+ "customProviderForm.submitError": {
+ "defaultMessage": "Failed to save provider. Please check your configuration and try again."
+ },
+ "customProviderForm.supportsStreaming": {
+ "defaultMessage": "Provider supports streaming responses"
+ },
+ "customProviderForm.toolCalling": {
+ "defaultMessage": "Tool calling"
+ },
+ "customProviderForm.updateProvider": {
+ "defaultMessage": "Update Provider"
+ },
+ "customProviderForm.usingTemplate": {
+ "defaultMessage": "Using template: {name}"
+ },
+ "customProviderForm.valuePlaceholder": {
+ "defaultMessage": "Value"
+ },
+ "defaultCardButtons.configureSettings": {
+ "defaultMessage": "Configure {name} settings"
+ },
+ "defaultCardButtons.deleteSettings": {
+ "defaultMessage": "Delete {name} settings"
+ },
+ "defaultCardButtons.editSettings": {
+ "defaultMessage": "Edit {name} settings"
+ },
+ "defaultCardButtons.getStarted": {
+ "defaultMessage": "Get started with goose!"
+ },
+ "defaultProviderSetupForm.apiHostLabel": {
+ "defaultMessage": "API Host"
+ },
+ "defaultProviderSetupForm.apiHostPlaceholder": {
+ "defaultMessage": "https://api.example.com"
+ },
+ "defaultProviderSetupForm.apiKeyLabel": {
+ "defaultMessage": "API Key"
+ },
+ "defaultProviderSetupForm.apiKeyPlaceholder": {
+ "defaultMessage": "Your API key"
+ },
+ "defaultProviderSetupForm.hideOptions": {
+ "defaultMessage": "Hide {count} options"
+ },
+ "defaultProviderSetupForm.loadingConfig": {
+ "defaultMessage": "Loading configuration values..."
+ },
+ "defaultProviderSetupForm.modelsLabel": {
+ "defaultMessage": "Models"
+ },
+ "defaultProviderSetupForm.modelsPlaceholder": {
+ "defaultMessage": "model-a, model-b"
+ },
+ "defaultProviderSetupForm.noConfigParameters": {
+ "defaultMessage": "No configuration parameters for this provider."
+ },
+ "defaultProviderSetupForm.showOptions": {
+ "defaultMessage": "Show {count} options"
+ },
+ "diagnosticsModal.attachHint": {
+ "defaultMessage": "If you file a bug, consider attaching the diagnostics report to it."
+ },
+ "diagnosticsModal.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "diagnosticsModal.configSettings": {
+ "defaultMessage": "Configuration settings"
+ },
+ "diagnosticsModal.description": {
+ "defaultMessage": "You can download a diagnostics zip file to share with the team, or file a bug directly on GitHub with your system details pre-filled. A diagnostics report contains the following:"
+ },
+ "diagnosticsModal.diagnosticsErrorMsg": {
+ "defaultMessage": "Failed to download diagnostics"
+ },
+ "diagnosticsModal.diagnosticsErrorTitle": {
+ "defaultMessage": "Diagnostics Error"
+ },
+ "diagnosticsModal.download": {
+ "defaultMessage": "Download"
+ },
+ "diagnosticsModal.downloading": {
+ "defaultMessage": "Downloading..."
+ },
+ "diagnosticsModal.fileBug": {
+ "defaultMessage": "File Bug on GitHub"
+ },
+ "diagnosticsModal.logFiles": {
+ "defaultMessage": "Recent log files"
+ },
+ "diagnosticsModal.opening": {
+ "defaultMessage": "Opening..."
+ },
+ "diagnosticsModal.reportProblem": {
+ "defaultMessage": "Report a Problem"
+ },
+ "diagnosticsModal.sensitiveWarning": {
+ "defaultMessage": "If your session contains sensitive information, do not share the diagnostics file publicly."
+ },
+ "diagnosticsModal.sessionMessages": {
+ "defaultMessage": "Your current session messages"
+ },
+ "diagnosticsModal.systemInfo": {
+ "defaultMessage": "Basic system info"
+ },
+ "diagnosticsModal.systemInfoErrorMsg": {
+ "defaultMessage": "Failed to get system information"
+ },
+ "diagnosticsModal.systemInfoErrorTitle": {
+ "defaultMessage": "Error"
+ },
+ "dialog.close": {
+ "defaultMessage": "Close"
+ },
+ "dictationSettings.addApiKey": {
+ "defaultMessage": "Add API Key"
+ },
+ "dictationSettings.apiKey": {
+ "defaultMessage": "API Key"
+ },
+ "dictationSettings.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "dictationSettings.chooseVoiceConversion": {
+ "defaultMessage": "Choose how voice is converted to text"
+ },
+ "dictationSettings.configureApiKey": {
+ "defaultMessage": "Configure the API key in {settingsPath} "
+ },
+ "dictationSettings.configured": {
+ "defaultMessage": "(Configured)"
+ },
+ "dictationSettings.configuredIn": {
+ "defaultMessage": "✓ Configured in {settingsPath}"
+ },
+ "dictationSettings.disabled": {
+ "defaultMessage": "Disabled"
+ },
+ "dictationSettings.enterApiKey": {
+ "defaultMessage": "Enter your API key"
+ },
+ "dictationSettings.notConfigured": {
+ "defaultMessage": "(not configured)"
+ },
+ "dictationSettings.removeApiKey": {
+ "defaultMessage": "Remove API Key"
+ },
+ "dictationSettings.requiredForTranscription": {
+ "defaultMessage": "Required for transcription"
+ },
+ "dictationSettings.save": {
+ "defaultMessage": "Save"
+ },
+ "dictationSettings.updateApiKey": {
+ "defaultMessage": "Update API Key"
+ },
+ "dictationSettings.voiceDictationProvider": {
+ "defaultMessage": "Voice Dictation Provider"
+ },
+ "dirSwitcher.failedToUpdateWorkingDir": {
+ "defaultMessage": "Failed to update working directory"
+ },
+ "elicitationRequest.cancelled": {
+ "defaultMessage": "Information request was cancelled."
+ },
+ "elicitationRequest.defaultMessage": {
+ "defaultMessage": "Goose needs some information from you."
+ },
+ "elicitationRequest.expired": {
+ "defaultMessage": "This request has expired. The extension will need to ask again."
+ },
+ "elicitationRequest.submit": {
+ "defaultMessage": "Submit"
+ },
+ "elicitationRequest.submitted": {
+ "defaultMessage": "Information submitted"
+ },
+ "elicitationRequest.waitingForResponse": {
+ "defaultMessage": "Waiting for your response ({timeRemaining} remaining)"
+ },
+ "envVarsSection.add": {
+ "defaultMessage": "Add"
+ },
+ "envVarsSection.bothRequired": {
+ "defaultMessage": "Both variable name and value must be entered"
+ },
+ "envVarsSection.envVarsDescription": {
+ "defaultMessage": "Add key-value pairs for environment variables. Click the \"+\" button to add after filling both fields. For existing secret values, click the edit button to modify."
+ },
+ "envVarsSection.environmentVariables": {
+ "defaultMessage": "Environment Variables"
+ },
+ "envVarsSection.noSpaces": {
+ "defaultMessage": "Variable name cannot contain spaces"
+ },
+ "envVarsSection.value": {
+ "defaultMessage": "Value"
+ },
+ "envVarsSection.variableName": {
+ "defaultMessage": "Variable name"
+ },
+ "environmentBadge.alpha": {
+ "defaultMessage": "Alpha"
+ },
+ "environmentBadge.dev": {
+ "defaultMessage": "Dev"
+ },
+ "errorBoundary.errorGeneric": {
+ "defaultMessage": "An error occurred."
+ },
+ "errorBoundary.errorWithVersion": {
+ "defaultMessage": "An error occurred in Goose v{version}."
+ },
+ "errorBoundary.heading": {
+ "defaultMessage": "Honk!"
+ },
+ "errorBoundary.reload": {
+ "defaultMessage": "Reload"
+ },
+ "extensionConfigFields.commandLabel": {
+ "defaultMessage": "Command"
+ },
+ "extensionConfigFields.commandPlaceholder": {
+ "defaultMessage": "e.g. npx -y @modelcontextprotocol/my-extension [filepath]"
+ },
+ "extensionConfigFields.commandRequired": {
+ "defaultMessage": "Command is required"
+ },
+ "extensionConfigFields.endpointLabel": {
+ "defaultMessage": "Endpoint"
+ },
+ "extensionConfigFields.endpointPlaceholder": {
+ "defaultMessage": "Enter endpoint URL..."
+ },
+ "extensionConfigFields.endpointRequired": {
+ "defaultMessage": "Endpoint URL is required"
+ },
+ "extensionInfoFields.descriptionLabel": {
+ "defaultMessage": "Description"
+ },
+ "extensionInfoFields.descriptionPlaceholder": {
+ "defaultMessage": "Optional description..."
+ },
+ "extensionInfoFields.extensionName": {
+ "defaultMessage": "Extension Name"
+ },
+ "extensionInfoFields.extensionNamePlaceholder": {
+ "defaultMessage": "Enter extension name..."
+ },
+ "extensionInfoFields.nameRequired": {
+ "defaultMessage": "Name is required"
+ },
+ "extensionInfoFields.typeHttp": {
+ "defaultMessage": "HTTP"
+ },
+ "extensionInfoFields.typeLabel": {
+ "defaultMessage": "Type"
+ },
+ "extensionInfoFields.typeSseUnsupported": {
+ "defaultMessage": "SSE (unsupported)"
+ },
+ "extensionInfoFields.typeStandardIo": {
+ "defaultMessage": "Standard IO (STDIO)"
+ },
+ "extensionInfoFields.typeStdio": {
+ "defaultMessage": "STDIO"
+ },
+ "extensionInfoFields.typeStreamableHttp": {
+ "defaultMessage": "Streamable HTTP"
+ },
+ "extensionInstallModal.alreadyInstalledMessage": {
+ "defaultMessage": "''{name}'' extension has already been installed successfully. Start a new chat session to use it."
+ },
+ "extensionInstallModal.alreadyInstalledTitle": {
+ "defaultMessage": "Extension ''{name}'' Already Installed"
+ },
+ "extensionInstallModal.blockedMessage": {
+ "defaultMessage": "This extension command is not in the allowed list and its installation is blocked. Extension: {name} Command: {command} Contact your administrator to request approval for this extension."
+ },
+ "extensionInstallModal.blockedTitle": {
+ "defaultMessage": "Extension Installation Blocked"
+ },
+ "extensionInstallModal.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "extensionInstallModal.installAnyway": {
+ "defaultMessage": "Install Anyway"
+ },
+ "extensionInstallModal.installing": {
+ "defaultMessage": "Installing..."
+ },
+ "extensionInstallModal.no": {
+ "defaultMessage": "No"
+ },
+ "extensionInstallModal.ok": {
+ "defaultMessage": "OK"
+ },
+ "extensionInstallModal.trustedMessage": {
+ "defaultMessage": "Are you sure you want to install the {name} extension? Command: {command}"
+ },
+ "extensionInstallModal.trustedTitle": {
+ "defaultMessage": "Confirm Extension Installation"
+ },
+ "extensionInstallModal.unknownCommand": {
+ "defaultMessage": "Unknown Command"
+ },
+ "extensionInstallModal.untrustedMessageWithCommand": {
+ "defaultMessage": "{securityMessage} Extension: {name} Command: {command} Contact your administrator if you are unsure about this."
+ },
+ "extensionInstallModal.untrustedMessageWithUrl": {
+ "defaultMessage": "{securityMessage} Extension: {name} URL: {url} Contact your administrator if you are unsure about this."
+ },
+ "extensionInstallModal.untrustedSecurityMessage": {
+ "defaultMessage": "This extension command is not in the allowed list and will be able to access your conversations and provide additional functionality. Installing extensions from untrusted sources may pose security risks."
+ },
+ "extensionInstallModal.untrustedTitle": {
+ "defaultMessage": "Install Untrusted Extension?"
+ },
+ "extensionInstallModal.yes": {
+ "defaultMessage": "Yes"
+ },
+ "extensionItem.configureExtension": {
+ "defaultMessage": "Configure {name} Extension"
+ },
+ "extensionItem.toggleExtension": {
+ "defaultMessage": "Toggle {name} extension On or Off"
+ },
+ "extensionList.availableExtensions": {
+ "defaultMessage": "Available Extensions ({count})"
+ },
+ "extensionList.builtInExtension": {
+ "defaultMessage": "Built-in extension"
+ },
+ "extensionList.defaultExtensions": {
+ "defaultMessage": "Default Extensions ({count})"
+ },
+ "extensionList.noExtensions": {
+ "defaultMessage": "No extensions available"
+ },
+ "extensionModal.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "extensionModal.closeWithoutSaving": {
+ "defaultMessage": "Close Without Saving"
+ },
+ "extensionModal.confirmRemoval": {
+ "defaultMessage": "Confirm removal"
+ },
+ "extensionModal.deleteDescription": {
+ "defaultMessage": "This will permanently remove this extension and all of its settings."
+ },
+ "extensionModal.deleteExtensionTitle": {
+ "defaultMessage": "Delete Extension \"{name}\""
+ },
+ "extensionModal.installationNotes": {
+ "defaultMessage": "Installation Notes"
+ },
+ "extensionModal.removeExtension": {
+ "defaultMessage": "Remove extension"
+ },
+ "extensionModal.unsavedChangesMessage": {
+ "defaultMessage": "You have unsaved changes to the extension configuration. Are you sure you want to close without saving?"
+ },
+ "extensionModal.unsavedChangesTitle": {
+ "defaultMessage": "Unsaved Changes"
+ },
+ "extensionTimeoutField.timeoutLabel": {
+ "defaultMessage": "Timeout"
+ },
+ "extensionsSection.addCustomExtension": {
+ "defaultMessage": "Add custom extension"
+ },
+ "extensionsSection.addExtension": {
+ "defaultMessage": "Add Extension"
+ },
+ "extensionsSection.browseExtensions": {
+ "defaultMessage": "Browse extensions"
+ },
+ "extensionsSection.saveChanges": {
+ "defaultMessage": "Save Changes"
+ },
+ "extensionsSection.updateExtension": {
+ "defaultMessage": "Update Extension"
+ },
+ "extensionsView.addCustomExtension": {
+ "defaultMessage": "Add custom extension"
+ },
+ "extensionsView.addExtension": {
+ "defaultMessage": "Add Extension"
+ },
+ "extensionsView.browseExtensions": {
+ "defaultMessage": "Browse extensions"
+ },
+ "extensionsView.defaultNote": {
+ "defaultMessage": "Extensions enabled here are used as the default for new chats. You can also toggle active extensions during chat."
+ },
+ "extensionsView.description": {
+ "defaultMessage": "These extensions use the Model Context Protocol (MCP). They can expand Goose's capabilities using three main components: Prompts, Resources, and Tools. {searchShortcut} to search."
+ },
+ "extensionsView.heading": {
+ "defaultMessage": "Extensions"
+ },
+ "extensionsView.searchPlaceholder": {
+ "defaultMessage": "Search extensions..."
+ },
+ "externalBackendSection.description": {
+ "defaultMessage": "By default goose launches a server for you, use this to connect to an external goose server"
+ },
+ "externalBackendSection.restartNote": {
+ "defaultMessage": "Changes require restarting Goose to take effect. New chat windows will connect to the external server."
+ },
+ "externalBackendSection.secretKey": {
+ "defaultMessage": "Secret Key"
+ },
+ "externalBackendSection.secretKeyHelp": {
+ "defaultMessage": "The secret key configured on the goosed server (GOOSE_SERVER__SECRET_KEY)"
+ },
+ "externalBackendSection.secretKeyPlaceholder": {
+ "defaultMessage": "Enter the server's secret key"
+ },
+ "externalBackendSection.serverUrl": {
+ "defaultMessage": "Server URL"
+ },
+ "externalBackendSection.title": {
+ "defaultMessage": "Goose Server"
+ },
+ "externalBackendSection.urlFormatError": {
+ "defaultMessage": "Invalid URL format"
+ },
+ "externalBackendSection.urlProtocolError": {
+ "defaultMessage": "URL must use http or https protocol"
+ },
+ "externalBackendSection.useExternalServer": {
+ "defaultMessage": "Use external server"
+ },
+ "externalBackendSection.useExternalServerDescription": {
+ "defaultMessage": "Connect to a goose server running elsewhere (requires app restart)"
+ },
+ "freeOptionCards.chooseOption": {
+ "defaultMessage": "Choose an option to get started."
+ },
+ "freeOptionCards.freeAndPrivate": {
+ "defaultMessage": "Free & Private"
+ },
+ "freeOptionCards.localModelDescription": {
+ "defaultMessage": "Download a model and run entirely on your machine. No API keys, no accounts."
+ },
+ "freeOptionCards.localModelTitle": {
+ "defaultMessage": "Use a Local Model"
+ },
+ "freeOptionCards.nanogptDescription": {
+ "defaultMessage": "Sign up to receive 60M free tokens for 7 days."
+ },
+ "freeOptionCards.nanogptTitle": {
+ "defaultMessage": "NanoGPT"
+ },
+ "freeOptionCards.retry": {
+ "defaultMessage": "Retry"
+ },
+ "freeOptionCards.tetrateDescription": {
+ "defaultMessage": "Access multiple AI models with automatic setup. Sign up to receive $10 credit."
+ },
+ "freeOptionCards.tetrateTitle": {
+ "defaultMessage": "Agent Router by Tetrate"
+ },
+ "freeOptionCards.unexpectedError": {
+ "defaultMessage": "An unexpected error occurred during setup."
+ },
+ "gatewaySettings.botFatherInstructions": {
+ "defaultMessage": "Open @BotFather on your phone, send /newbot, and follow the prompts to name your bot. BotFather will reply with an API token — paste it below."
+ },
+ "gatewaySettings.close": {
+ "defaultMessage": "Close"
+ },
+ "gatewaySettings.expiresIn": {
+ "defaultMessage": "Expires in {time}"
+ },
+ "gatewaySettings.failedToGeneratePairingCode": {
+ "defaultMessage": "Failed to generate pairing code"
+ },
+ "gatewaySettings.failedToRemove": {
+ "defaultMessage": "Failed to remove"
+ },
+ "gatewaySettings.failedToStart": {
+ "defaultMessage": "Failed to start"
+ },
+ "gatewaySettings.failedToStop": {
+ "defaultMessage": "Failed to stop"
+ },
+ "gatewaySettings.failedToUnpairUser": {
+ "defaultMessage": "Failed to unpair user"
+ },
+ "gatewaySettings.loading": {
+ "defaultMessage": "Loading..."
+ },
+ "gatewaySettings.pairDevice": {
+ "defaultMessage": "Pair Device"
+ },
+ "gatewaySettings.pairedUsers": {
+ "defaultMessage": "Paired Users"
+ },
+ "gatewaySettings.pairingCode": {
+ "defaultMessage": "Pairing Code"
+ },
+ "gatewaySettings.pasteBotToken": {
+ "defaultMessage": "Paste bot token here"
+ },
+ "gatewaySettings.remove": {
+ "defaultMessage": "Remove"
+ },
+ "gatewaySettings.running": {
+ "defaultMessage": "Running"
+ },
+ "gatewaySettings.sendCodeToPair": {
+ "defaultMessage": "Send this code to your {gatewayType} bot to pair."
+ },
+ "gatewaySettings.start": {
+ "defaultMessage": "Start"
+ },
+ "gatewaySettings.stop": {
+ "defaultMessage": "Stop"
+ },
+ "gatewaySettings.stopped": {
+ "defaultMessage": "Stopped"
+ },
+ "gatewaySettings.telegram": {
+ "defaultMessage": "Telegram"
+ },
+ "goosehintsModal.close": {
+ "defaultMessage": "Close"
+ },
+ "goosehintsModal.developer": {
+ "defaultMessage": "Developer"
+ },
+ "goosehintsModal.dialogDescription": {
+ "defaultMessage": "Provide additional context about your project to improve communication with Goose"
+ },
+ "goosehintsModal.dialogTitle": {
+ "defaultMessage": "Configure Project Hints (.goosehints)"
+ },
+ "goosehintsModal.errorReading": {
+ "defaultMessage": "Error reading .goosehints file: {error}"
+ },
+ "goosehintsModal.failedToAccess": {
+ "defaultMessage": "Failed to access .goosehints file"
+ },
+ "goosehintsModal.failedToSave": {
+ "defaultMessage": "Failed to save .goosehints file"
+ },
+ "goosehintsModal.fileCreating": {
+ "defaultMessage": "Creating new .goosehints file at: {filePath}"
+ },
+ "goosehintsModal.fileFound": {
+ "defaultMessage": ".goosehints file found at: {filePath}"
+ },
+ "goosehintsModal.helpText1": {
+ "defaultMessage": ".goosehints is a text file used to provide additional context about your project and improve the communication with Goose."
+ },
+ "goosehintsModal.helpText2": {
+ "defaultMessage": "Please make sure {bold} extension is enabled in the extensions page. This extension is required to use .goosehints. You'll need to restart your session for .goosehints updates to take effect."
+ },
+ "goosehintsModal.helpText3": {
+ "defaultMessage": "See {link} for more information."
+ },
+ "goosehintsModal.helpTextLink": {
+ "defaultMessage": "using .goosehints"
+ },
+ "goosehintsModal.placeholder": {
+ "defaultMessage": "Enter project hints here..."
+ },
+ "goosehintsModal.save": {
+ "defaultMessage": "Save"
+ },
+ "goosehintsModal.savedSuccessfully": {
+ "defaultMessage": "Saved successfully"
+ },
+ "goosehintsModal.saving": {
+ "defaultMessage": "Saving..."
+ },
+ "goosehintsSection.configure": {
+ "defaultMessage": "Configure"
+ },
+ "goosehintsSection.description": {
+ "defaultMessage": "Configure your project's .goosehints file to provide additional context to Goose"
+ },
+ "goosehintsSection.title": {
+ "defaultMessage": "Project Hints (.goosehints)"
+ },
+ "greeting.readyToBuild": {
+ "defaultMessage": "Ready to build something amazing?"
+ },
+ "greeting.readyToCreateGreat": {
+ "defaultMessage": "Ready to create something great?"
+ },
+ "greeting.readyToGetStarted": {
+ "defaultMessage": "Ready to get started?"
+ },
+ "greeting.whatCanBeAchieved": {
+ "defaultMessage": "What can be achieved?"
+ },
+ "greeting.whatCanBeBuilt": {
+ "defaultMessage": "What can be built today?"
+ },
+ "greeting.whatNeedsToBeDone": {
+ "defaultMessage": "What needs to be done?"
+ },
+ "greeting.whatProgress": {
+ "defaultMessage": "What progress can be made?"
+ },
+ "greeting.whatProjectNeedsAttention": {
+ "defaultMessage": "What project needs attention?"
+ },
+ "greeting.whatProjectReadyToBegin": {
+ "defaultMessage": "What project is ready to begin?"
+ },
+ "greeting.whatShallWeCreate": {
+ "defaultMessage": "What shall we create today?"
+ },
+ "greeting.whatTaskAwaits": {
+ "defaultMessage": "What task awaits?"
+ },
+ "greeting.whatToAccomplish": {
+ "defaultMessage": "What would you like to accomplish?"
+ },
+ "greeting.whatToExplore": {
+ "defaultMessage": "What would you like to explore?"
+ },
+ "greeting.whatToTackle": {
+ "defaultMessage": "What would you like to tackle?"
+ },
+ "greeting.whatToWorkOn": {
+ "defaultMessage": "What would you like to work on?"
+ },
+ "greeting.whatsNextChallenge": {
+ "defaultMessage": "What's the next challenge?"
+ },
+ "greeting.whatsOnYourMind": {
+ "defaultMessage": "What's on your mind?"
+ },
+ "greeting.whatsTheMission": {
+ "defaultMessage": "What's the mission today?"
+ },
+ "greeting.whatsThePlan": {
+ "defaultMessage": "What's the plan for today?"
+ },
+ "groupedExtensionLoadingToast.askGoose": {
+ "defaultMessage": "Ask goose"
+ },
+ "groupedExtensionLoadingToast.collapseDetails": {
+ "defaultMessage": "Collapse details"
+ },
+ "groupedExtensionLoadingToast.copied": {
+ "defaultMessage": "Copied!"
+ },
+ "groupedExtensionLoadingToast.copyError": {
+ "defaultMessage": "Copy error"
+ },
+ "groupedExtensionLoadingToast.expandDetails": {
+ "defaultMessage": "Expand details"
+ },
+ "groupedExtensionLoadingToast.failedToAddExtension": {
+ "defaultMessage": "Failed to add extension"
+ },
+ "groupedExtensionLoadingToast.failedToLoad": {
+ "defaultMessage": "{count,plural,one{# extension failed to load} other{# extensions failed to load}}"
+ },
+ "groupedExtensionLoadingToast.loadingExtensions": {
+ "defaultMessage": "{count,plural,one{Loading # extension...} other{Loading # extensions...}}"
+ },
+ "groupedExtensionLoadingToast.partiallyLoaded": {
+ "defaultMessage": "{totalCount,plural,one{Loaded {successCount}/# extension} other{Loaded {successCount}/# extensions}}"
+ },
+ "groupedExtensionLoadingToast.showDetails": {
+ "defaultMessage": "Show details"
+ },
+ "groupedExtensionLoadingToast.showLess": {
+ "defaultMessage": "Show less"
+ },
+ "groupedExtensionLoadingToast.successfullyLoaded": {
+ "defaultMessage": "{count,plural,one{Successfully loaded # extension} other{Successfully loaded # extensions}}"
+ },
+ "headersSection.add": {
+ "defaultMessage": "Add"
+ },
+ "headersSection.bothRequired": {
+ "defaultMessage": "Both header name and value must be entered"
+ },
+ "headersSection.duplicateHeader": {
+ "defaultMessage": "A header with this name already exists"
+ },
+ "headersSection.headerName": {
+ "defaultMessage": "Header name"
+ },
+ "headersSection.headersDescription": {
+ "defaultMessage": "Add custom HTTP headers to include in requests to the MCP server. Click the \"+\" button to add after filling both fields."
+ },
+ "headersSection.noSpaces": {
+ "defaultMessage": "Header name cannot contain spaces"
+ },
+ "headersSection.requestHeaders": {
+ "defaultMessage": "Request Headers"
+ },
+ "headersSection.value": {
+ "defaultMessage": "Value"
+ },
+ "huggingFaceModelSearch.directDownload": {
+ "defaultMessage": "Direct Download"
+ },
+ "huggingFaceModelSearch.directDownloadDescription": {
+ "defaultMessage": "Specify a model directly: {format}"
+ },
+ "huggingFaceModelSearch.directDownloadErrorMsg": {
+ "defaultMessage": "Failed to start the download. Check the spec: {error}"
+ },
+ "huggingFaceModelSearch.directDownloadFailed": {
+ "defaultMessage": "Direct download failed"
+ },
+ "huggingFaceModelSearch.download": {
+ "defaultMessage": "Download"
+ },
+ "huggingFaceModelSearch.loadingVariants": {
+ "defaultMessage": "Loading variants..."
+ },
+ "huggingFaceModelSearch.noGgufModels": {
+ "defaultMessage": "No GGUF models found for this query."
+ },
+ "huggingFaceModelSearch.recommended": {
+ "defaultMessage": "Recommended"
+ },
+ "huggingFaceModelSearch.searchError": {
+ "defaultMessage": "Search error: {details}"
+ },
+ "huggingFaceModelSearch.searchFailed": {
+ "defaultMessage": "Search failed. Please try again."
+ },
+ "huggingFaceModelSearch.searchHuggingFace": {
+ "defaultMessage": "Search HuggingFace"
+ },
+ "huggingFaceModelSearch.searchNoData": {
+ "defaultMessage": "Search returned no data."
+ },
+ "huggingFaceModelSearch.searchPlaceholder": {
+ "defaultMessage": "Search for GGUF models..."
+ },
+ "imagePreview.altText": {
+ "defaultMessage": "goose image"
+ },
+ "imagePreview.clickToCollapse": {
+ "defaultMessage": "Click to collapse"
+ },
+ "imagePreview.clickToExpand": {
+ "defaultMessage": "Click to expand"
+ },
+ "imagePreview.unableToLoad": {
+ "defaultMessage": "Unable to load image"
+ },
+ "importRecipeForm.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "importRecipeForm.deeplinkHint": {
+ "defaultMessage": "Paste a recipe deeplink starting with \"goose://recipe?config=\""
+ },
+ "importRecipeForm.deeplinkPlaceholder": {
+ "defaultMessage": "Paste your goose://recipe?config=... deeplink here"
+ },
+ "importRecipeForm.example": {
+ "defaultMessage": "example"
+ },
+ "importRecipeForm.expectedRecipeStructure": {
+ "defaultMessage": "Expected Recipe Structure"
+ },
+ "importRecipeForm.importRecipeButton": {
+ "defaultMessage": "Import Recipe"
+ },
+ "importRecipeForm.importRecipeTitle": {
+ "defaultMessage": "Import Recipe"
+ },
+ "importRecipeForm.importing": {
+ "defaultMessage": "Importing..."
+ },
+ "importRecipeForm.or": {
+ "defaultMessage": "OR"
+ },
+ "importRecipeForm.recipeDeeplinkLabel": {
+ "defaultMessage": "Recipe Deeplink"
+ },
+ "importRecipeForm.recipeFileHint": {
+ "defaultMessage": "Upload a YAML or JSON file containing the recipe structure"
+ },
+ "importRecipeForm.recipeFileLabel": {
+ "defaultMessage": "Recipe File"
+ },
+ "importRecipeForm.reviewWarning": {
+ "defaultMessage": "Ensure you review contents of recipe files before adding them to your goose interface."
+ },
+ "importRecipeForm.schemaDescription": {
+ "defaultMessage": "Your YAML or JSON file should follow this structure. Required fields are: title, description, and either instructions or prompt."
+ },
+ "inlineEditText.clickToEdit": {
+ "defaultMessage": "Click to edit"
+ },
+ "inlineEditText.doubleClickToEdit": {
+ "defaultMessage": "Double-click to edit"
+ },
+ "inlineEditText.enterText": {
+ "defaultMessage": "Enter text"
+ },
+ "inlineEditText.failedToSave": {
+ "defaultMessage": "Failed to save"
+ },
+ "instructionsEditor.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "instructionsEditor.insertExample": {
+ "defaultMessage": "Insert Example"
+ },
+ "instructionsEditor.label": {
+ "defaultMessage": "Instructions"
+ },
+ "instructionsEditor.placeholder": {
+ "defaultMessage": "Detailed instructions for the AI, hidden from the user"
+ },
+ "instructionsEditor.save": {
+ "defaultMessage": "Save Instructions"
+ },
+ "instructionsEditor.syntaxHelp": {
+ "defaultMessage": "Use {code} syntax to define parameters that users can fill in"
+ },
+ "instructionsEditor.title": {
+ "defaultMessage": "Instructions Editor"
+ },
+ "jsonSchemaEditor.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "jsonSchemaEditor.description": {
+ "defaultMessage": "Define the expected structure of the AI's response using JSON Schema format"
+ },
+ "jsonSchemaEditor.insertExample": {
+ "defaultMessage": "Insert Example"
+ },
+ "jsonSchemaEditor.invalidJson": {
+ "defaultMessage": "Invalid JSON format"
+ },
+ "jsonSchemaEditor.label": {
+ "defaultMessage": "Response JSON Schema"
+ },
+ "jsonSchemaEditor.save": {
+ "defaultMessage": "Save Schema"
+ },
+ "jsonSchemaEditor.title": {
+ "defaultMessage": "JSON Schema Editor"
+ },
+ "jsonSchemaForm.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "jsonSchemaForm.fieldRequired": {
+ "defaultMessage": "This field is required"
+ },
+ "jsonSchemaForm.maxLength": {
+ "defaultMessage": "Maximum length is {maxLength}"
+ },
+ "jsonSchemaForm.maxValue": {
+ "defaultMessage": "Maximum value is {maximum}"
+ },
+ "jsonSchemaForm.minLength": {
+ "defaultMessage": "Minimum length is {minLength}"
+ },
+ "jsonSchemaForm.minValue": {
+ "defaultMessage": "Minimum value is {minimum}"
+ },
+ "jsonSchemaForm.noFields": {
+ "defaultMessage": "No fields to display"
+ },
+ "jsonSchemaForm.selectPlaceholder": {
+ "defaultMessage": "Select..."
+ },
+ "jsonSchemaForm.submit": {
+ "defaultMessage": "Submit"
+ },
+ "keyValueEditor.addValue": {
+ "defaultMessage": "Add pre-configured value"
+ },
+ "keyValueEditor.defaultKeyPlaceholder": {
+ "defaultMessage": "Parameter name..."
+ },
+ "keyValueEditor.defaultValuePlaceholder": {
+ "defaultMessage": "Parameter value..."
+ },
+ "keyValueEditor.removeValue": {
+ "defaultMessage": "Remove pre-configured value {key}"
+ },
+ "keyboardShortcuts.alwaysOnTopDescription": {
+ "defaultMessage": "Toggle window always on top"
+ },
+ "keyboardShortcuts.alwaysOnTopLabel": {
+ "defaultMessage": "Always on Top"
+ },
+ "keyboardShortcuts.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "keyboardShortcuts.categoryApplication": {
+ "defaultMessage": "Application Shortcuts"
+ },
+ "keyboardShortcuts.categoryApplicationDescription": {
+ "defaultMessage": "These shortcuts work when Goose is the active application"
+ },
+ "keyboardShortcuts.categoryGlobal": {
+ "defaultMessage": "Global Shortcuts"
+ },
+ "keyboardShortcuts.categoryGlobalDescription": {
+ "defaultMessage": "These shortcuts work system-wide, even when Goose is not focused"
+ },
+ "keyboardShortcuts.categorySearch": {
+ "defaultMessage": "Search Shortcuts"
+ },
+ "keyboardShortcuts.categorySearchDescription": {
+ "defaultMessage": "These shortcuts work when searching in a conversation"
+ },
+ "keyboardShortcuts.categoryWindow": {
+ "defaultMessage": "Window Shortcuts"
+ },
+ "keyboardShortcuts.categoryWindowDescription": {
+ "defaultMessage": "These shortcuts control window behavior"
+ },
+ "keyboardShortcuts.change": {
+ "defaultMessage": "Change"
+ },
+ "keyboardShortcuts.disabled": {
+ "defaultMessage": "Disabled"
+ },
+ "keyboardShortcuts.dismiss": {
+ "defaultMessage": "Dismiss"
+ },
+ "keyboardShortcuts.findDescription": {
+ "defaultMessage": "Open search in conversation"
+ },
+ "keyboardShortcuts.findLabel": {
+ "defaultMessage": "Find"
+ },
+ "keyboardShortcuts.findNextDescription": {
+ "defaultMessage": "Jump to next search result"
+ },
+ "keyboardShortcuts.findNextLabel": {
+ "defaultMessage": "Find Next"
+ },
+ "keyboardShortcuts.findPreviousDescription": {
+ "defaultMessage": "Jump to previous search result"
+ },
+ "keyboardShortcuts.findPreviousLabel": {
+ "defaultMessage": "Find Previous"
+ },
+ "keyboardShortcuts.focusWindowDescription": {
+ "defaultMessage": "Bring Goose window to front from anywhere"
+ },
+ "keyboardShortcuts.focusWindowLabel": {
+ "defaultMessage": "Focus Goose Window"
+ },
+ "keyboardShortcuts.loading": {
+ "defaultMessage": "Loading..."
+ },
+ "keyboardShortcuts.newChatDescription": {
+ "defaultMessage": "Create a new chat in the current window"
+ },
+ "keyboardShortcuts.newChatLabel": {
+ "defaultMessage": "New Chat"
+ },
+ "keyboardShortcuts.newChatWindowDescription": {
+ "defaultMessage": "Open a new Goose window"
+ },
+ "keyboardShortcuts.newChatWindowLabel": {
+ "defaultMessage": "New Chat Window"
+ },
+ "keyboardShortcuts.openDirectoryDescription": {
+ "defaultMessage": "Open directory selection dialog"
+ },
+ "keyboardShortcuts.openDirectoryLabel": {
+ "defaultMessage": "Open Directory"
+ },
+ "keyboardShortcuts.quickLauncherDescription": {
+ "defaultMessage": "Open the quick launcher overlay"
+ },
+ "keyboardShortcuts.quickLauncherLabel": {
+ "defaultMessage": "Quick Launcher"
+ },
+ "keyboardShortcuts.reassignShortcut": {
+ "defaultMessage": "Reassign Shortcut"
+ },
+ "keyboardShortcuts.resetAllShortcuts": {
+ "defaultMessage": "Reset All Shortcuts"
+ },
+ "keyboardShortcuts.resetShortcutsDetail": {
+ "defaultMessage": "This will restore all shortcuts to their original configuration."
+ },
+ "keyboardShortcuts.resetShortcutsMessage": {
+ "defaultMessage": "Reset all keyboard shortcuts to their default values?"
+ },
+ "keyboardShortcuts.resetShortcutsTitle": {
+ "defaultMessage": "Reset Keyboard Shortcuts"
+ },
+ "keyboardShortcuts.resetToDefaultsDescription": {
+ "defaultMessage": "Restore all keyboard shortcuts to their original configuration"
+ },
+ "keyboardShortcuts.resetToDefaultsHeading": {
+ "defaultMessage": "Reset to Defaults"
+ },
+ "keyboardShortcuts.restartDescription": {
+ "defaultMessage": "Changes to application shortcuts (like New Chat, Settings, etc.) require restarting Goose to take effect. Global shortcuts (Focus Window, Quick Launcher) work immediately."
+ },
+ "keyboardShortcuts.restartRequired": {
+ "defaultMessage": "Restart Required"
+ },
+ "keyboardShortcuts.settingsDescription": {
+ "defaultMessage": "Open settings panel"
+ },
+ "keyboardShortcuts.settingsLabel": {
+ "defaultMessage": "Settings"
+ },
+ "keyboardShortcuts.shortcutConflictSaveDetail": {
+ "defaultMessage": "Saving this will remove the shortcut from \"{conflictLabel}\" and assign it to \"{targetLabel}\". Do you want to continue?"
+ },
+ "keyboardShortcuts.shortcutConflictTitle": {
+ "defaultMessage": "Shortcut Conflict"
+ },
+ "keyboardShortcuts.shortcutConflictToggleDetail": {
+ "defaultMessage": "Enabling this will remove the shortcut from \"{conflictLabel}\" and assign it to \"{targetLabel}\". Do you want to continue?"
+ },
+ "keyboardShortcuts.shortcutConflictToggleMessage": {
+ "defaultMessage": "The shortcut {shortcut} is already assigned to \"{conflictLabel}\"."
+ },
+ "keyboardShortcuts.toggleNavigationDescription": {
+ "defaultMessage": "Show or hide the navigation menu"
+ },
+ "keyboardShortcuts.toggleNavigationLabel": {
+ "defaultMessage": "Toggle Navigation"
+ },
+ "launcher.placeholder": {
+ "defaultMessage": "Ask goose anything..."
+ },
+ "loadingGoose.compacting": {
+ "defaultMessage": "goose is compacting the conversation..."
+ },
+ "loadingGoose.idle": {
+ "defaultMessage": "goose is working on it…"
+ },
+ "loadingGoose.loadingConversation": {
+ "defaultMessage": "loading conversation..."
+ },
+ "loadingGoose.restartingAgent": {
+ "defaultMessage": "restarting session..."
+ },
+ "loadingGoose.streaming": {
+ "defaultMessage": "goose is working on it…"
+ },
+ "loadingGoose.thinking": {
+ "defaultMessage": "goose is thinking…"
+ },
+ "loadingGoose.waiting": {
+ "defaultMessage": "goose is waiting…"
+ },
+ "localInferenceSettings.deleteConfirm": {
+ "defaultMessage": "Delete this model? You can re-download it later."
+ },
+ "localInferenceSettings.description": {
+ "defaultMessage": "Download and manage local LLM models for inference without API keys. Search HuggingFace for any GGUF model or use the featured picks below."
+ },
+ "localInferenceSettings.download": {
+ "defaultMessage": "Download"
+ },
+ "localInferenceSettings.downloadFailed": {
+ "defaultMessage": "Download failed"
+ },
+ "localInferenceSettings.downloadProgress": {
+ "defaultMessage": "{downloaded} / {total} ({percent}%)"
+ },
+ "localInferenceSettings.downloadedModels": {
+ "defaultMessage": "Downloaded Models"
+ },
+ "localInferenceSettings.downloading": {
+ "defaultMessage": "Downloading"
+ },
+ "localInferenceSettings.featuredModels": {
+ "defaultMessage": "Featured Models"
+ },
+ "localInferenceSettings.modelSettings": {
+ "defaultMessage": "Model Settings"
+ },
+ "localInferenceSettings.modelSettingsTitle": {
+ "defaultMessage": "Model settings"
+ },
+ "localInferenceSettings.noModels": {
+ "defaultMessage": "No models available"
+ },
+ "localInferenceSettings.recommended": {
+ "defaultMessage": "Recommended"
+ },
+ "localInferenceSettings.remaining": {
+ "defaultMessage": "{time} remaining"
+ },
+ "localInferenceSettings.showAllFeatured": {
+ "defaultMessage": "Show all featured ({count} more)"
+ },
+ "localInferenceSettings.showRecommendedOnly": {
+ "defaultMessage": "Show recommended only"
+ },
+ "localInferenceSettings.title": {
+ "defaultMessage": "Local Inference Models"
+ },
+ "localModelManager.active": {
+ "defaultMessage": "Active"
+ },
+ "localModelManager.deleteConfirm": {
+ "defaultMessage": "Delete this model? You can re-download it later."
+ },
+ "localModelManager.download": {
+ "defaultMessage": "Download"
+ },
+ "localModelManager.downloaded": {
+ "defaultMessage": "Downloaded"
+ },
+ "localModelManager.gpuAcceleration": {
+ "defaultMessage": "Supports GPU acceleration (CUDA for NVIDIA, Metal for Apple Silicon). GPU features must be enabled at build time for hardware acceleration."
+ },
+ "localModelManager.noModels": {
+ "defaultMessage": "No models available"
+ },
+ "localModelManager.recommended": {
+ "defaultMessage": "Recommended"
+ },
+ "localModelManager.recommendedForHardware": {
+ "defaultMessage": "Recommended for your hardware"
+ },
+ "localModelManager.showAllModels": {
+ "defaultMessage": "Show all models ({count} more)"
+ },
+ "localModelManager.showRecommendedOnly": {
+ "defaultMessage": "Show recommended only"
+ },
+ "localModelPicker.back": {
+ "defaultMessage": "Back"
+ },
+ "localModelPicker.bestForMachine": {
+ "defaultMessage": "Best for your machine"
+ },
+ "localModelPicker.cancelDownload": {
+ "defaultMessage": "Cancel Download"
+ },
+ "localModelPicker.checkingModels": {
+ "defaultMessage": "Checking available models..."
+ },
+ "localModelPicker.downloadModel": {
+ "defaultMessage": "Download {modelId} ({size})"
+ },
+ "localModelPicker.downloading": {
+ "defaultMessage": "Downloading {modelId}"
+ },
+ "localModelPicker.failedToLoad": {
+ "defaultMessage": "Failed to load available models. Please try again."
+ },
+ "localModelPicker.failedToStartDownload": {
+ "defaultMessage": "Failed to start download. Please try again."
+ },
+ "localModelPicker.hideOtherSizes": {
+ "defaultMessage": "Hide other sizes"
+ },
+ "localModelPicker.localModelsNote": {
+ "defaultMessage": "Local models keep everything on your machine for full privacy. Performance and context window size may vary compared to cloud providers depending on your hardware and model size."
+ },
+ "localModelPicker.lostConnection": {
+ "defaultMessage": "Lost connection to download. Please try again."
+ },
+ "localModelPicker.modelNotFound": {
+ "defaultMessage": "Model not found"
+ },
+ "localModelPicker.ready": {
+ "defaultMessage": "Ready"
+ },
+ "localModelPicker.selectModel": {
+ "defaultMessage": "Select a model"
+ },
+ "localModelPicker.showOtherSizes": {
+ "defaultMessage": "Show {count} other sizes"
+ },
+ "localModelPicker.startingDownload": {
+ "defaultMessage": "Starting download..."
+ },
+ "localModelPicker.tryAgain": {
+ "defaultMessage": "Try Again"
+ },
+ "localModelPicker.useModel": {
+ "defaultMessage": "Use {modelId}"
+ },
+ "markdownContent.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "markdownContent.copyCode": {
+ "defaultMessage": "Copy code"
+ },
+ "markdownContent.failedToOpenLink": {
+ "defaultMessage": "Failed to Open Link"
+ },
+ "markdownContent.noApplicationFound": {
+ "defaultMessage": "No application found to open this link."
+ },
+ "markdownContent.open": {
+ "defaultMessage": "Open"
+ },
+ "markdownContent.openExternalLink": {
+ "defaultMessage": "Open External Link"
+ },
+ "markdownContent.openProtocolLink": {
+ "defaultMessage": "Open {protocol} link?"
+ },
+ "markdownContent.thisWillOpen": {
+ "defaultMessage": "This will open: {href}"
+ },
+ "mcpAppRenderer.appFallbackTitle": {
+ "defaultMessage": "App"
+ },
+ "mcpAppRenderer.cancelButton": {
+ "defaultMessage": "Cancel"
+ },
+ "mcpAppRenderer.close": {
+ "defaultMessage": "Close"
+ },
+ "mcpAppRenderer.exitFullscreen": {
+ "defaultMessage": "Exit fullscreen"
+ },
+ "mcpAppRenderer.exitFullscreenTitle": {
+ "defaultMessage": "Exit fullscreen (Esc)"
+ },
+ "mcpAppRenderer.failedToInitSandbox": {
+ "defaultMessage": "Failed to initialize sandbox proxy"
+ },
+ "mcpAppRenderer.failedToLoadResource": {
+ "defaultMessage": "Failed to load resource"
+ },
+ "mcpAppRenderer.fullscreen": {
+ "defaultMessage": "Fullscreen"
+ },
+ "mcpAppRenderer.invalidUrl": {
+ "defaultMessage": "Invalid URL"
+ },
+ "mcpAppRenderer.movePipWindow": {
+ "defaultMessage": "Move Picture-in-Picture window (use arrow keys)"
+ },
+ "mcpAppRenderer.openButton": {
+ "defaultMessage": "Open"
+ },
+ "mcpAppRenderer.openExternalLinkTitle": {
+ "defaultMessage": "Open External Link"
+ },
+ "mcpAppRenderer.openLinkDetail": {
+ "defaultMessage": "This will open: {url}"
+ },
+ "mcpAppRenderer.openProtocolLink": {
+ "defaultMessage": "Open {protocol} link?"
+ },
+ "mcpAppRenderer.pictureInPicture": {
+ "defaultMessage": "Picture-in-Picture"
+ },
+ "mcpAppRenderer.playingInPip": {
+ "defaultMessage": "Playing in Picture-in-Picture"
+ },
+ "mcpUIResourceRenderer.cancelButton": {
+ "defaultMessage": "Cancel"
+ },
+ "mcpUIResourceRenderer.openButton": {
+ "defaultMessage": "Open"
+ },
+ "mcpUIResourceRenderer.openExternalLinkTitle": {
+ "defaultMessage": "Open External Link"
+ },
+ "mcpUIResourceRenderer.openLinkDetail": {
+ "defaultMessage": "This will open: {url}"
+ },
+ "mcpUIResourceRenderer.openProtocolLink": {
+ "defaultMessage": "Open {protocol} link?"
+ },
+ "mcpUIResourceRenderer.toastMessageReceived": {
+ "defaultMessage": "Message received for {message}."
+ },
+ "mcpUIResourceRenderer.toastTitle": {
+ "defaultMessage": "MCP-UI {messageType} message"
+ },
+ "mcpUIResourceRenderer.toastUnsupported": {
+ "defaultMessage": "Message received for {message}. {messageType} messages aren't supported yet, refer to console for more details."
+ },
+ "mentionPopover.itemsFound": {
+ "defaultMessage": "{count,plural,one{# item found} other{# items found}}"
+ },
+ "mentionPopover.noItemsFound": {
+ "defaultMessage": "No items found matching \"{query}\""
+ },
+ "mentionPopover.scanningFiles": {
+ "defaultMessage": "Scanning files..."
+ },
+ "messageCopyLink.copied": {
+ "defaultMessage": "Copied!"
+ },
+ "messageCopyLink.copy": {
+ "defaultMessage": "Copy"
+ },
+ "messageQueue.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "messageQueue.cannotSendWhileEditing": {
+ "defaultMessage": "Cannot send while editing"
+ },
+ "messageQueue.clearAll": {
+ "defaultMessage": "Clear All"
+ },
+ "messageQueue.clickToEdit": {
+ "defaultMessage": "{content} (Click to edit)"
+ },
+ "messageQueue.collapseQueue": {
+ "defaultMessage": "Collapse queue"
+ },
+ "messageQueue.dragToReorder": {
+ "defaultMessage": "Drag messages to reorder priority"
+ },
+ "messageQueue.expandQueue": {
+ "defaultMessage": "Expand queue"
+ },
+ "messageQueue.messageCount": {
+ "defaultMessage": "{count,plural,one{# message {status}} other{# messages {status}}}"
+ },
+ "messageQueue.messageQueue": {
+ "defaultMessage": "Message Queue"
+ },
+ "messageQueue.next": {
+ "defaultMessage": "Next"
+ },
+ "messageQueue.paused": {
+ "defaultMessage": "Paused"
+ },
+ "messageQueue.queuePaused": {
+ "defaultMessage": "Queue Paused"
+ },
+ "messageQueue.queuePausedCompact": {
+ "defaultMessage": "Queue paused - click \"Send\" or add new message to resume"
+ },
+ "messageQueue.queuePausedExpanded": {
+ "defaultMessage": "Queue paused by interruption. Use \"Send Now\" or add a new message to resume."
+ },
+ "messageQueue.queued": {
+ "defaultMessage": "queued"
+ },
+ "messageQueue.removeFromQueue": {
+ "defaultMessage": "Remove this message from queue"
+ },
+ "messageQueue.save": {
+ "defaultMessage": "Save"
+ },
+ "messageQueue.sendNow": {
+ "defaultMessage": "Send this message now"
+ },
+ "messageQueue.stopAndSend": {
+ "defaultMessage": "Stop current processing and send this message now"
+ },
+ "messageQueue.waiting": {
+ "defaultMessage": "waiting"
+ },
+ "microphoneSelector.chooseDescription": {
+ "defaultMessage": "Choose which microphone to use for dictation"
+ },
+ "microphoneSelector.grantAccess": {
+ "defaultMessage": "Grant Access"
+ },
+ "microphoneSelector.grantAccessDescription": {
+ "defaultMessage": "Grant access to see available microphones"
+ },
+ "microphoneSelector.microphone": {
+ "defaultMessage": "Microphone"
+ },
+ "microphoneSelector.microphoneLabel": {
+ "defaultMessage": "Microphone {index}"
+ },
+ "microphoneSelector.selectedMicrophone": {
+ "defaultMessage": "Selected Microphone"
+ },
+ "microphoneSelector.speakToTest": {
+ "defaultMessage": "Speak to test your microphone ({seconds}s)"
+ },
+ "microphoneSelector.stop": {
+ "defaultMessage": "Stop"
+ },
+ "microphoneSelector.systemDefault": {
+ "defaultMessage": "System Default"
+ },
+ "microphoneSelector.test": {
+ "defaultMessage": "Test"
+ },
+ "modeSelectionItem.autonomousDescription": {
+ "defaultMessage": "Full file modification capabilities, edit, create, and delete files freely."
+ },
+ "modeSelectionItem.autonomousLabel": {
+ "defaultMessage": "Autonomous"
+ },
+ "modeSelectionItem.chatOnlyDescription": {
+ "defaultMessage": "Engage with the selected provider without using tools or extensions."
+ },
+ "modeSelectionItem.chatOnlyLabel": {
+ "defaultMessage": "Chat only"
+ },
+ "modeSelectionItem.manualDescription": {
+ "defaultMessage": "All tools, extensions and file modifications will require human approval"
+ },
+ "modeSelectionItem.manualLabel": {
+ "defaultMessage": "Manual"
+ },
+ "modeSelectionItem.smartDescription": {
+ "defaultMessage": "Intelligently determine which actions need approval based on risk level"
+ },
+ "modeSelectionItem.smartLabel": {
+ "defaultMessage": "Smart"
+ },
+ "modelAndProviderContext.modelChangeFailed": {
+ "defaultMessage": "{provider}/{model} failed"
+ },
+ "modelAndProviderContext.modelChangedTitle": {
+ "defaultMessage": "Model changed"
+ },
+ "modelAndProviderContext.selectModel": {
+ "defaultMessage": "Select Model"
+ },
+ "modelAndProviderContext.switchModelSuccess": {
+ "defaultMessage": "Successfully switched models -- using {model} from {provider}"
+ },
+ "modelAndProviderContext.unknownProviderMsg": {
+ "defaultMessage": "Unknown provider in config -- please inspect your config.yaml"
+ },
+ "modelAndProviderContext.unknownProviderTitle": {
+ "defaultMessage": "Provider name lookup"
+ },
+ "modelSettingsButtons.configureProviders": {
+ "defaultMessage": "Configure providers"
+ },
+ "modelSettingsButtons.switchModels": {
+ "defaultMessage": "Switch models"
+ },
+ "modelSettingsPanel.batchSize": {
+ "defaultMessage": "Batch size"
+ },
+ "modelSettingsPanel.batchSizeDescription": {
+ "defaultMessage": "Prompt processing batch"
+ },
+ "modelSettingsPanel.contextAndGeneration": {
+ "defaultMessage": "Context & Generation"
+ },
+ "modelSettingsPanel.contextSize": {
+ "defaultMessage": "Context size"
+ },
+ "modelSettingsPanel.contextSizeDescription": {
+ "defaultMessage": "Max context window (0 = model default)"
+ },
+ "modelSettingsPanel.etaLearningRate": {
+ "defaultMessage": "Eta (learning rate)"
+ },
+ "modelSettingsPanel.flashAttention": {
+ "defaultMessage": "Flash attention"
+ },
+ "modelSettingsPanel.flashAttentionDescription": {
+ "defaultMessage": "Enable flash attention optimization"
+ },
+ "modelSettingsPanel.frequencyPenalty": {
+ "defaultMessage": "Frequency penalty"
+ },
+ "modelSettingsPanel.frequencyPenaltyDescription": {
+ "defaultMessage": "0.0 = off"
+ },
+ "modelSettingsPanel.gpuLayers": {
+ "defaultMessage": "GPU layers"
+ },
+ "modelSettingsPanel.gpuLayersDescription": {
+ "defaultMessage": "Layers to offload to GPU"
+ },
+ "modelSettingsPanel.loadingSettings": {
+ "defaultMessage": "Loading settings..."
+ },
+ "modelSettingsPanel.lockModelInRam": {
+ "defaultMessage": "Lock model in RAM (mlock)"
+ },
+ "modelSettingsPanel.lockModelInRamDescription": {
+ "defaultMessage": "Prevent model from being swapped to disk"
+ },
+ "modelSettingsPanel.maxOutputTokens": {
+ "defaultMessage": "Max output tokens"
+ },
+ "modelSettingsPanel.maxOutputTokensDescription": {
+ "defaultMessage": "Cap on generated tokens"
+ },
+ "modelSettingsPanel.minP": {
+ "defaultMessage": "Min P"
+ },
+ "modelSettingsPanel.nativeToolCalling": {
+ "defaultMessage": "Native tool calling"
+ },
+ "modelSettingsPanel.nativeToolCallingDescription": {
+ "defaultMessage": "Use the model's built-in tool-call format instead of the shell-command emulator. Enable for large models that reliably support tool calling."
+ },
+ "modelSettingsPanel.performance": {
+ "defaultMessage": "Performance"
+ },
+ "modelSettingsPanel.presencePenalty": {
+ "defaultMessage": "Presence penalty"
+ },
+ "modelSettingsPanel.presencePenaltyDescription": {
+ "defaultMessage": "0.0 = off"
+ },
+ "modelSettingsPanel.repeatPenalty": {
+ "defaultMessage": "Repeat penalty"
+ },
+ "modelSettingsPanel.repeatPenaltyDescription": {
+ "defaultMessage": "1.0 = off"
+ },
+ "modelSettingsPanel.repeatWindow": {
+ "defaultMessage": "Repeat window"
+ },
+ "modelSettingsPanel.repeatWindowDescription": {
+ "defaultMessage": "Tokens to look back"
+ },
+ "modelSettingsPanel.repetitionPenalty": {
+ "defaultMessage": "Repetition Penalty"
+ },
+ "modelSettingsPanel.reset": {
+ "defaultMessage": "Reset"
+ },
+ "modelSettingsPanel.resetToDefaults": {
+ "defaultMessage": "Reset to defaults"
+ },
+ "modelSettingsPanel.samplingStrategy": {
+ "defaultMessage": "Sampling Strategy"
+ },
+ "modelSettingsPanel.saving": {
+ "defaultMessage": "Saving..."
+ },
+ "modelSettingsPanel.seed": {
+ "defaultMessage": "Seed"
+ },
+ "modelSettingsPanel.tauTargetEntropy": {
+ "defaultMessage": "Tau (target entropy)"
+ },
+ "modelSettingsPanel.temperature": {
+ "defaultMessage": "Temperature"
+ },
+ "modelSettingsPanel.threads": {
+ "defaultMessage": "Threads"
+ },
+ "modelSettingsPanel.threadsDescription": {
+ "defaultMessage": "CPU threads for generation"
+ },
+ "modelSettingsPanel.toolCalling": {
+ "defaultMessage": "Tool Calling"
+ },
+ "modelSettingsPanel.topK": {
+ "defaultMessage": "Top K"
+ },
+ "modelSettingsPanel.topP": {
+ "defaultMessage": "Top P"
+ },
+ "modelsBottomBar.changeModel": {
+ "defaultMessage": "Change Model"
+ },
+ "modelsBottomBar.currentModel": {
+ "defaultMessage": "Current model"
+ },
+ "modelsBottomBar.localModelSettings": {
+ "defaultMessage": "Local Model Settings"
+ },
+ "modelsBottomBar.localModelSettingsTitle": {
+ "defaultMessage": "Local Model Settings — {modelName}"
+ },
+ "modelsBottomBar.selectModel": {
+ "defaultMessage": "Select Model"
+ },
+ "modelsSection.resetDescription": {
+ "defaultMessage": "Clear your selected model and provider settings to start fresh"
+ },
+ "modelsSection.resetTitle": {
+ "defaultMessage": "Reset Provider and Model"
+ },
+ "navigationCustomization.dragInstructions": {
+ "defaultMessage": "Drag to reorder, click the eye icon to show/hide items"
+ },
+ "navigationCustomization.hideItem": {
+ "defaultMessage": "Hide item"
+ },
+ "navigationCustomization.itemApps": {
+ "defaultMessage": "Apps"
+ },
+ "navigationCustomization.itemChat": {
+ "defaultMessage": "Chat"
+ },
+ "navigationCustomization.itemExtensions": {
+ "defaultMessage": "Extensions"
+ },
+ "navigationCustomization.itemHome": {
+ "defaultMessage": "Home"
+ },
+ "navigationCustomization.itemRecipes": {
+ "defaultMessage": "Recipes"
+ },
+ "navigationCustomization.itemScheduler": {
+ "defaultMessage": "Scheduler"
+ },
+ "navigationCustomization.itemSettings": {
+ "defaultMessage": "Settings"
+ },
+ "navigationCustomization.resetToDefaults": {
+ "defaultMessage": "Reset to defaults"
+ },
+ "navigationCustomization.showItem": {
+ "defaultMessage": "Show item"
+ },
+ "navigationModeSelector.overlayDescription": {
+ "defaultMessage": "Full-screen overlay"
+ },
+ "navigationModeSelector.overlayLabel": {
+ "defaultMessage": "Overlay"
+ },
+ "navigationModeSelector.pushDescription": {
+ "defaultMessage": "Navigation pushes content"
+ },
+ "navigationModeSelector.pushLabel": {
+ "defaultMessage": "Push"
+ },
+ "navigationPositionSelector.bottomLabel": {
+ "defaultMessage": "Bottom"
+ },
+ "navigationPositionSelector.leftLabel": {
+ "defaultMessage": "Left"
+ },
+ "navigationPositionSelector.rightLabel": {
+ "defaultMessage": "Right"
+ },
+ "navigationPositionSelector.topLabel": {
+ "defaultMessage": "Top"
+ },
+ "navigationStyleSelector.listDescription": {
+ "defaultMessage": "Classic condensed view"
+ },
+ "navigationStyleSelector.listLabel": {
+ "defaultMessage": "List"
+ },
+ "navigationStyleSelector.tileDescription": {
+ "defaultMessage": "Enlarged tile view"
+ },
+ "navigationStyleSelector.tileLabel": {
+ "defaultMessage": "Tile"
+ },
+ "onboardingGuard.welcomeDescription": {
+ "defaultMessage": "Your local AI agent. Connect an AI model provider to get started."
+ },
+ "onboardingGuard.welcomeTitle": {
+ "defaultMessage": "Welcome to goose"
+ },
+ "onboardingSuccess.allSet": {
+ "defaultMessage": "You're all set to start using goose."
+ },
+ "onboardingSuccess.connectedTo": {
+ "defaultMessage": "Connected to {providerName}"
+ },
+ "onboardingSuccess.getStarted": {
+ "defaultMessage": "Get Started"
+ },
+ "onboardingSuccess.learnMore": {
+ "defaultMessage": "Learn more"
+ },
+ "onboardingSuccess.localModelReady": {
+ "defaultMessage": "Local model ready"
+ },
+ "onboardingSuccess.privacyDescription": {
+ "defaultMessage": "Anonymous usage data helps improve goose. We never collect your conversations, code, or personal data."
+ },
+ "onboardingSuccess.privacyTitle": {
+ "defaultMessage": "Privacy"
+ },
+ "onboardingSuccess.shareUsageData": {
+ "defaultMessage": "Share anonymous usage data"
+ },
+ "parameterInput.defaultValue": {
+ "defaultMessage": "Default Value"
+ },
+ "parameterInput.defaultValuePlaceholder": {
+ "defaultMessage": "Enter default value"
+ },
+ "parameterInput.deleteParameter": {
+ "defaultMessage": "Delete parameter: {key}"
+ },
+ "parameterInput.description": {
+ "defaultMessage": "description"
+ },
+ "parameterInput.descriptionHelp": {
+ "defaultMessage": "This is the message the end-user will see."
+ },
+ "parameterInput.descriptionPlaceholder": {
+ "defaultMessage": "E.g., \"Enter the name for the new component\""
+ },
+ "parameterInput.inputType": {
+ "defaultMessage": "Input Type"
+ },
+ "parameterInput.optional": {
+ "defaultMessage": "Optional"
+ },
+ "parameterInput.optionsHelp": {
+ "defaultMessage": "Enter each option on a new line. These will be shown as dropdown choices."
+ },
+ "parameterInput.optionsLabel": {
+ "defaultMessage": "Options (one per line)"
+ },
+ "parameterInput.optionsPlaceholder": {
+ "defaultMessage": "Option 1 Option 2 Option 3"
+ },
+ "parameterInput.required": {
+ "defaultMessage": "Required"
+ },
+ "parameterInput.requirement": {
+ "defaultMessage": "Requirement"
+ },
+ "parameterInput.typeBoolean": {
+ "defaultMessage": "Boolean"
+ },
+ "parameterInput.typeNumber": {
+ "defaultMessage": "Number"
+ },
+ "parameterInput.typeSelect": {
+ "defaultMessage": "Select"
+ },
+ "parameterInput.typeString": {
+ "defaultMessage": "String"
+ },
+ "parameterInput.unused": {
+ "defaultMessage": "Unused"
+ },
+ "parameterInput.unusedWarningTitle": {
+ "defaultMessage": "This parameter is not used in the instructions or prompt. It will be available for manual input but may not be needed."
+ },
+ "parameterInputModal.backToForm": {
+ "defaultMessage": "Back to Parameter Form"
+ },
+ "parameterInputModal.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "parameterInputModal.cancelRecipeSetup": {
+ "defaultMessage": "Cancel Recipe Setup"
+ },
+ "parameterInputModal.enterValue": {
+ "defaultMessage": "Enter value for {key}..."
+ },
+ "parameterInputModal.false": {
+ "defaultMessage": "False"
+ },
+ "parameterInputModal.recipeParameters": {
+ "defaultMessage": "Recipe Parameters"
+ },
+ "parameterInputModal.select": {
+ "defaultMessage": "Select..."
+ },
+ "parameterInputModal.selectOption": {
+ "defaultMessage": "Select an option..."
+ },
+ "parameterInputModal.startNewChat": {
+ "defaultMessage": "Start New Chat (No Recipe)"
+ },
+ "parameterInputModal.startRecipe": {
+ "defaultMessage": "Start Recipe"
+ },
+ "parameterInputModal.true": {
+ "defaultMessage": "True"
+ },
+ "parameterInputModal.whatToDo": {
+ "defaultMessage": "What would you like to do?"
+ },
+ "permissionModal.alwaysAllow": {
+ "defaultMessage": "Always allow"
+ },
+ "permissionModal.askBefore": {
+ "defaultMessage": "Ask before"
+ },
+ "permissionModal.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "permissionModal.close": {
+ "defaultMessage": "Close"
+ },
+ "permissionModal.failedToLoadTools": {
+ "defaultMessage": "Failed to load tools"
+ },
+ "permissionModal.failedToLoadToolsDescription": {
+ "defaultMessage": "Could not load tools for this extension. The extension may not be loaded in the current session."
+ },
+ "permissionModal.neverAllow": {
+ "defaultMessage": "Never allow"
+ },
+ "permissionModal.noActiveSession": {
+ "defaultMessage": "No active session"
+ },
+ "permissionModal.noActiveSessionDescription": {
+ "defaultMessage": "Start a chat session first to configure tool permissions for this extension. Tool permissions are loaded from the active session's extensions."
+ },
+ "permissionModal.noToolsAvailable": {
+ "defaultMessage": "No tools available for this extension."
+ },
+ "permissionModal.saveChanges": {
+ "defaultMessage": "Save Changes"
+ },
+ "permissionRulesModal.description": {
+ "defaultMessage": "Configure tool permissions for extensions to control how they interact with your system."
+ },
+ "permissionRulesModal.extensionRules": {
+ "defaultMessage": "Extension rules"
+ },
+ "permissionRulesModal.title": {
+ "defaultMessage": "Permission Rules"
+ },
+ "permissionSetting.extensionRules": {
+ "defaultMessage": "Extension rules"
+ },
+ "permissionSetting.permissionRules": {
+ "defaultMessage": "Permission Rules"
+ },
+ "permissionSetting.permissionRulesDescription": {
+ "defaultMessage": "Hidden instructions that will be passed to the provider to help direct and add context to your responses."
+ },
+ "popularChatTopics.governmentForms": {
+ "defaultMessage": "Describe in detail how various forms of government works and rank each by units of geese"
+ },
+ "popularChatTopics.heading": {
+ "defaultMessage": "Popular chat topics"
+ },
+ "popularChatTopics.organizePhotos": {
+ "defaultMessage": "Organize the photos on my desktop into neat little folders by subject matter"
+ },
+ "popularChatTopics.start": {
+ "defaultMessage": "Start"
+ },
+ "popularChatTopics.tamagotchiGame": {
+ "defaultMessage": "Develop a tamagotchi game that lives on my computer and follows a pixelated styling"
+ },
+ "privacyInfoModal.collectErrors": {
+ "defaultMessage": "Error types (e.g., \"rate_limit\", \"auth\" - no details)"
+ },
+ "privacyInfoModal.collectExtensions": {
+ "defaultMessage": "Extensions and tool usage counts (names only)"
+ },
+ "privacyInfoModal.collectOs": {
+ "defaultMessage": "Operating system, version, and architecture"
+ },
+ "privacyInfoModal.collectProvider": {
+ "defaultMessage": "Provider and model used"
+ },
+ "privacyInfoModal.collectSession": {
+ "defaultMessage": "Session metrics (duration, interaction count, token usage)"
+ },
+ "privacyInfoModal.collectVersion": {
+ "defaultMessage": "goose version and install method"
+ },
+ "privacyInfoModal.description": {
+ "defaultMessage": "Anonymous usage data helps us understand how goose is used and identify areas for improvement."
+ },
+ "privacyInfoModal.neverCollect": {
+ "defaultMessage": "We never collect your conversations, code, tool arguments, error messages, or any personal data. You can change this setting anytime in Settings."
+ },
+ "privacyInfoModal.title": {
+ "defaultMessage": "Privacy details"
+ },
+ "privacyInfoModal.whatWeCollect": {
+ "defaultMessage": "What we collect:"
+ },
+ "progressiveMessageList.loadingMessages": {
+ "defaultMessage": "Loading messages... ({renderedCount}/{totalCount})"
+ },
+ "progressiveMessageList.searchHint": {
+ "defaultMessage": "Press Cmd/Ctrl+F to load all messages immediately for search"
+ },
+ "promptsSettings.allPromptsReset": {
+ "defaultMessage": "All prompts reset to defaults"
+ },
+ "promptsSettings.backToList": {
+ "defaultMessage": "Back to List"
+ },
+ "promptsSettings.confirmReplaceWithDefault": {
+ "defaultMessage": "Replace current content with default? Your changes will be lost."
+ },
+ "promptsSettings.confirmResetAll": {
+ "defaultMessage": "Are you sure you want to reset all prompts to their defaults? This cannot be undone."
+ },
+ "promptsSettings.confirmResetOne": {
+ "defaultMessage": "Are you sure you want to reset this prompt to its default? This cannot be undone."
+ },
+ "promptsSettings.confirmUnsavedBack": {
+ "defaultMessage": "You have unsaved changes. Are you sure you want to go back?"
+ },
+ "promptsSettings.customized": {
+ "defaultMessage": "Customized"
+ },
+ "promptsSettings.edit": {
+ "defaultMessage": "Edit"
+ },
+ "promptsSettings.editPromptTitle": {
+ "defaultMessage": "Edit: {name}"
+ },
+ "promptsSettings.editingLabel": {
+ "defaultMessage": "Editing: {name}"
+ },
+ "promptsSettings.enterPromptContent": {
+ "defaultMessage": "Enter prompt content..."
+ },
+ "promptsSettings.failedToLoadPrompt": {
+ "defaultMessage": "Failed to load prompt"
+ },
+ "promptsSettings.failedToLoadPrompts": {
+ "defaultMessage": "Failed to load prompts"
+ },
+ "promptsSettings.failedToResetPrompt": {
+ "defaultMessage": "Failed to reset prompt"
+ },
+ "promptsSettings.failedToResetPrompts": {
+ "defaultMessage": "Failed to reset prompts"
+ },
+ "promptsSettings.failedToSavePrompt": {
+ "defaultMessage": "Failed to save prompt"
+ },
+ "promptsSettings.promptEditingDescription": {
+ "defaultMessage": "Customize the prompts that define goose's behavior in different contexts. These prompts use Jinja2 templating syntax. Be careful when modifying template variables, as incorrect changes can break functionality. Please share any improvements with the community."
+ },
+ "promptsSettings.promptEditingTitle": {
+ "defaultMessage": "Prompt Editing"
+ },
+ "promptsSettings.promptResetToDefault": {
+ "defaultMessage": "Prompt reset to default"
+ },
+ "promptsSettings.promptSaved": {
+ "defaultMessage": "Prompt saved"
+ },
+ "promptsSettings.resetAll": {
+ "defaultMessage": "Reset All"
+ },
+ "promptsSettings.resetToDefault": {
+ "defaultMessage": "Reset to Default"
+ },
+ "promptsSettings.restoreDefault": {
+ "defaultMessage": "Restore Default"
+ },
+ "promptsSettings.save": {
+ "defaultMessage": "Save"
+ },
+ "promptsSettings.templateTip": {
+ "defaultMessage": "Template variables like {extensionsExample} or {forExample} are replaced with actual values at runtime. Be careful not to remove required variables."
+ },
+ "promptsSettings.unsavedChanges": {
+ "defaultMessage": "You have unsaved changes"
+ },
+ "providerCard.noMetadata": {
+ "defaultMessage": "ProviderCard error: No metadata provided"
+ },
+ "providerCard.unknownProvider": {
+ "defaultMessage": "Unknown Provider"
+ },
+ "providerCatalogPicker.anthropicCompatible": {
+ "defaultMessage": "Anthropic Compatible"
+ },
+ "providerCatalogPicker.apiFormat": {
+ "defaultMessage": "API Format"
+ },
+ "providerCatalogPicker.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "providerCatalogPicker.chooseProvider": {
+ "defaultMessage": "Choose Provider"
+ },
+ "providerCatalogPicker.errorPrefix": {
+ "defaultMessage": "Error: {error}"
+ },
+ "providerCatalogPicker.loadingProviders": {
+ "defaultMessage": "Loading providers..."
+ },
+ "providerCatalogPicker.modelsAvailable": {
+ "defaultMessage": "{count} models available"
+ },
+ "providerCatalogPicker.noProvidersAvailable": {
+ "defaultMessage": "No providers available"
+ },
+ "providerCatalogPicker.noProvidersFound": {
+ "defaultMessage": "No providers found for \"{query}\""
+ },
+ "providerCatalogPicker.openaiCompatible": {
+ "defaultMessage": "OpenAI Compatible"
+ },
+ "providerCatalogPicker.requiresEnvVar": {
+ "defaultMessage": "• Requires {envVar}"
+ },
+ "providerCatalogPicker.searchProviders": {
+ "defaultMessage": "Search providers..."
+ },
+ "providerCatalogPicker.selectFormatDescription": {
+ "defaultMessage": "Select an API format and provider. We'll auto-fill the configuration for you."
+ },
+ "providerConfigForm.browserWindowOpen": {
+ "defaultMessage": "A browser window will open for you to complete the login."
+ },
+ "providerConfigForm.configuring": {
+ "defaultMessage": "Configuring..."
+ },
+ "providerConfigForm.continue": {
+ "defaultMessage": "Continue"
+ },
+ "providerConfigForm.deviceCodeFlowHint": {
+ "defaultMessage": "A browser window will open and the verification code will be copied to your clipboard. Paste it in the browser to complete sign-in."
+ },
+ "providerConfigForm.noApiKey": {
+ "defaultMessage": "Don't have an API key?"
+ },
+ "providerConfigForm.signInWith": {
+ "defaultMessage": "Sign in with {providerName}"
+ },
+ "providerConfigForm.signingIn": {
+ "defaultMessage": "Signing in..."
+ },
+ "providerConfigurationModal.addApiKeyDescription": {
+ "defaultMessage": "Add your API key(s) for this provider to integrate into goose"
+ },
+ "providerConfigurationModal.browserWindowHint": {
+ "defaultMessage": "A browser window will open for you to complete the login."
+ },
+ "providerConfigurationModal.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "providerConfigurationModal.cannotDeleteActive": {
+ "defaultMessage": "You cannot delete this provider while it's currently in use. Please switch to a different model first."
+ },
+ "providerConfigurationModal.checkConfigAgain": {
+ "defaultMessage": "Check your configuration again to use this provider."
+ },
+ "providerConfigurationModal.close": {
+ "defaultMessage": "Close"
+ },
+ "providerConfigurationModal.configureHeader": {
+ "defaultMessage": "Configure {providerName}"
+ },
+ "providerConfigurationModal.deleteConfigHeader": {
+ "defaultMessage": "Delete configuration for {providerName}"
+ },
+ "providerConfigurationModal.deleteConfirmation": {
+ "defaultMessage": "This will permanently delete the current provider configuration."
+ },
+ "providerConfigurationModal.deviceCodeFlowHint": {
+ "defaultMessage": "A browser window will open and the verification code will be copied to your clipboard. Paste it in the browser to complete sign-in."
+ },
+ "providerConfigurationModal.errorCheckingConfig": {
+ "defaultMessage": "There was an error checking this provider configuration."
+ },
+ "providerConfigurationModal.errorTitle": {
+ "defaultMessage": "Error"
+ },
+ "providerConfigurationModal.externalSetupIntro": {
+ "defaultMessage": "This provider is configured outside of goose. Follow these steps:"
+ },
+ "providerConfigurationModal.goBack": {
+ "defaultMessage": "Go Back"
+ },
+ "providerConfigurationModal.oauthLoginFailed": {
+ "defaultMessage": "OAuth login failed: {error}"
+ },
+ "providerConfigurationModal.oauthSignInDescription": {
+ "defaultMessage": "Sign in with your {providerName} account to use this provider"
+ },
+ "providerConfigurationModal.parameterRequired": {
+ "defaultMessage": "{paramName} is required"
+ },
+ "providerConfigurationModal.removeConfiguration": {
+ "defaultMessage": "Remove Configuration"
+ },
+ "providerConfigurationModal.seeDocumentation": {
+ "defaultMessage": "See the documentation for more details."
+ },
+ "providerConfigurationModal.signInWith": {
+ "defaultMessage": "Sign in with {providerName}"
+ },
+ "providerConfigurationModal.signingIn": {
+ "defaultMessage": "Signing in..."
+ },
+ "providerGrid.addProvider": {
+ "defaultMessage": "Add Provider"
+ },
+ "providerGrid.addProviderTitle": {
+ "defaultMessage": "Add Provider"
+ },
+ "providerGrid.chooseModel": {
+ "defaultMessage": "Choose Model"
+ },
+ "providerGrid.configureProvider": {
+ "defaultMessage": "Configure Provider"
+ },
+ "providerGrid.editProvider": {
+ "defaultMessage": "Edit Provider"
+ },
+ "providerGrid.fromTemplateOrManual": {
+ "defaultMessage": "From template or manual setup"
+ },
+ "providerLogo.alt": {
+ "defaultMessage": "{providerName} logo"
+ },
+ "providerSelector.addCustomProvider": {
+ "defaultMessage": "Add a custom provider"
+ },
+ "providerSelector.addCustomProviderTitle": {
+ "defaultMessage": "Add Custom Provider"
+ },
+ "providerSelector.connectProvider": {
+ "defaultMessage": "Connect to a Provider"
+ },
+ "providerSelector.connectProviderDescription": {
+ "defaultMessage": "Connect OpenAI, Anthropic, Google, etc"
+ },
+ "providerSelector.freeLocalDescription": {
+ "defaultMessage": "Use a local model or a provider with free credits"
+ },
+ "providerSelector.selectProvider": {
+ "defaultMessage": "Select a provider"
+ },
+ "providerSelector.useFreeLocal": {
+ "defaultMessage": "Use Free/Local Providers"
+ },
+ "providerSettings.configurationSettings": {
+ "defaultMessage": "Provider Configuration Settings"
+ },
+ "providerSettings.loadingProviders": {
+ "defaultMessage": "Loading providers..."
+ },
+ "providerSettings.onboardingDescription": {
+ "defaultMessage": "Select an AI model provider to get started with goose. You'll need to use API keys generated by each provider which will be encrypted and stored locally. You can change your provider at any time in settings."
+ },
+ "providerSettings.otherProviders": {
+ "defaultMessage": "Other providers"
+ },
+ "providerSetupActions.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "providerSetupActions.cannotDeleteActive": {
+ "defaultMessage": "You cannot delete {providerName} while it's currently in use. Please switch to a different model before deleting this provider."
+ },
+ "providerSetupActions.confirmDelete": {
+ "defaultMessage": "Confirm Delete"
+ },
+ "providerSetupActions.confirmDeleteMessage": {
+ "defaultMessage": "Are you sure you want to delete the configuration parameters for {providerName}? This action cannot be undone."
+ },
+ "providerSetupActions.deleteProvider": {
+ "defaultMessage": "Delete Provider"
+ },
+ "providerSetupActions.enableProvider": {
+ "defaultMessage": "Enable Provider"
+ },
+ "providerSetupActions.ok": {
+ "defaultMessage": "Ok"
+ },
+ "providerSetupActions.submit": {
+ "defaultMessage": "Submit"
+ },
+ "recipeActivityEditor.activitiesDescription": {
+ "defaultMessage": "The top-line prompts and activity buttons that will display in the recipe chat window."
+ },
+ "recipeActivityEditor.activitiesLabel": {
+ "defaultMessage": "Activities"
+ },
+ "recipeActivityEditor.activityButtonsDescription": {
+ "defaultMessage": "Clickable buttons that will appear below the message to help users interact with your recipe."
+ },
+ "recipeActivityEditor.activityButtonsLabel": {
+ "defaultMessage": "Activity Buttons"
+ },
+ "recipeActivityEditor.addActivity": {
+ "defaultMessage": "Add activity"
+ },
+ "recipeActivityEditor.addNewActivityPlaceholder": {
+ "defaultMessage": "Add new activity..."
+ },
+ "recipeActivityEditor.messageDescription": {
+ "defaultMessage": "A formatted message that will appear at the top of the recipe. Supports markdown formatting."
+ },
+ "recipeActivityEditor.messageLabel": {
+ "defaultMessage": "Message"
+ },
+ "recipeActivityEditor.messagePlaceholder": {
+ "defaultMessage": "Enter a user facing introduction message for your recipe (supports **bold**, *italic*, `code`, etc.)"
+ },
+ "recipeExtensionSelector.description": {
+ "defaultMessage": "Select which extensions should be available when running this recipe. Leave empty to use default extensions."
+ },
+ "recipeExtensionSelector.extensionsSelected": {
+ "defaultMessage": "{count,plural,one{# extension selected} other{# extensions selected}}"
+ },
+ "recipeExtensionSelector.label": {
+ "defaultMessage": "Extensions (Optional)"
+ },
+ "recipeExtensionSelector.noExtensionsAvailable": {
+ "defaultMessage": "No extensions available"
+ },
+ "recipeExtensionSelector.noExtensionsFound": {
+ "defaultMessage": "No extensions found"
+ },
+ "recipeExtensionSelector.searchPlaceholder": {
+ "defaultMessage": "Search extensions..."
+ },
+ "recipeFormFields.addParameter": {
+ "defaultMessage": "Add parameter"
+ },
+ "recipeFormFields.advancedOptions": {
+ "defaultMessage": "Advanced Options"
+ },
+ "recipeFormFields.advancedOptionsHint": {
+ "defaultMessage": "Activities, parameters, model, extensions, response schema, subrecipes"
+ },
+ "recipeFormFields.descriptionLabel": {
+ "defaultMessage": "Description"
+ },
+ "recipeFormFields.descriptionPlaceholder": {
+ "defaultMessage": "Brief description of what this recipe does"
+ },
+ "recipeFormFields.enterValueFor": {
+ "defaultMessage": "Enter value for {key}"
+ },
+ "recipeFormFields.initialPrompt": {
+ "defaultMessage": "Initial Prompt"
+ },
+ "recipeFormFields.instructionsLabel": {
+ "defaultMessage": "Instructions"
+ },
+ "recipeFormFields.instructionsPlaceholder": {
+ "defaultMessage": "Detailed instructions for the AI, hidden from the user"
+ },
+ "recipeFormFields.openEditor": {
+ "defaultMessage": "Open Editor"
+ },
+ "recipeFormFields.parameterNamePlaceholder": {
+ "defaultMessage": "Enter parameter name..."
+ },
+ "recipeFormFields.parametersDescription": {
+ "defaultMessage": "Parameters will be automatically detected from '{{parameter_name}}' syntax in instructions/prompt/activities or you can manually add them below."
+ },
+ "recipeFormFields.parametersLabel": {
+ "defaultMessage": "Parameters"
+ },
+ "recipeFormFields.promptOptionalHint": {
+ "defaultMessage": "(Optional - Instructions or Prompt are required)"
+ },
+ "recipeFormFields.promptPlaceholder": {
+ "defaultMessage": "Pre-filled prompt when the recipe starts"
+ },
+ "recipeFormFields.responseJsonSchema": {
+ "defaultMessage": "Response JSON Schema"
+ },
+ "recipeFormFields.responseJsonSchemaDescription": {
+ "defaultMessage": "Define the expected structure of the AI's response using JSON Schema format"
+ },
+ "recipeFormFields.templateVarHint": {
+ "defaultMessage": "Use '{{parameter_name}}' to define parameters that can be filled in when running the recipe."
+ },
+ "recipeFormFields.titleLabel": {
+ "defaultMessage": "Title"
+ },
+ "recipeFormFields.titlePlaceholder": {
+ "defaultMessage": "Recipe title"
+ },
+ "recipeHeader.recipeLabel": {
+ "defaultMessage": "Recipe"
+ },
+ "recipeModelSelector.backToModelList": {
+ "defaultMessage": "Back to model list"
+ },
+ "recipeModelSelector.enterCustomModel": {
+ "defaultMessage": "Enter custom model name"
+ },
+ "recipeModelSelector.enterModelNotListed": {
+ "defaultMessage": "Enter a model not listed..."
+ },
+ "recipeModelSelector.fetchError": {
+ "defaultMessage": "Failed to fetch models. Please try again later."
+ },
+ "recipeModelSelector.loadingModels": {
+ "defaultMessage": "Loading models…"
+ },
+ "recipeModelSelector.modelHint": {
+ "defaultMessage": "Leave empty to use the default model for the selected provider"
+ },
+ "recipeModelSelector.modelLabel": {
+ "defaultMessage": "Model (Optional)"
+ },
+ "recipeModelSelector.providerHint": {
+ "defaultMessage": "Leave empty to use the default provider configured in settings"
+ },
+ "recipeModelSelector.providerLabel": {
+ "defaultMessage": "Provider (Optional)"
+ },
+ "recipeModelSelector.selectModel": {
+ "defaultMessage": "Select a model"
+ },
+ "recipeModelSelector.selectProvider": {
+ "defaultMessage": "Select provider"
+ },
+ "recipeModelSelector.useDefaultProvider": {
+ "defaultMessage": "Use default provider"
+ },
+ "recipeNameField.defaultLabel": {
+ "defaultMessage": "Recipe Name"
+ },
+ "recipeNameField.formatHint": {
+ "defaultMessage": "Will be automatically formatted (lowercase, dashes for spaces)"
+ },
+ "recipeWarningModal.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "recipeWarningModal.descriptionLabel": {
+ "defaultMessage": "Description:"
+ },
+ "recipeWarningModal.firstTimeDescription": {
+ "defaultMessage": "You are about to execute a recipe that you haven't run before."
+ },
+ "recipeWarningModal.hiddenCharsWarning": {
+ "defaultMessage": "This recipe contains hidden characters that will be ignored for your safety, as they could be used for malicious purposes."
+ },
+ "recipeWarningModal.instructionsLabel": {
+ "defaultMessage": "Instructions:"
+ },
+ "recipeWarningModal.newRecipeWarningTitle": {
+ "defaultMessage": "⚠️ New Recipe Warning"
+ },
+ "recipeWarningModal.recipePreview": {
+ "defaultMessage": "Recipe Preview:"
+ },
+ "recipeWarningModal.securityWarningTitle": {
+ "defaultMessage": "⚠️ Security Warning"
+ },
+ "recipeWarningModal.titleLabel": {
+ "defaultMessage": "Title:"
+ },
+ "recipeWarningModal.trustAndExecute": {
+ "defaultMessage": "Trust and Execute"
+ },
+ "recipeWarningModal.trustSource": {
+ "defaultMessage": "Only proceed if you trust the source of this recipe."
+ },
+ "recipesView.addSchedule": {
+ "defaultMessage": "Add schedule"
+ },
+ "recipesView.addSlashCommand": {
+ "defaultMessage": "Add slash command"
+ },
+ "recipesView.adjustSearchTerms": {
+ "defaultMessage": "Try adjusting your search terms"
+ },
+ "recipesView.allFiles": {
+ "defaultMessage": "All Files"
+ },
+ "recipesView.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "recipesView.copyDeeplink": {
+ "defaultMessage": "Copy Deeplink"
+ },
+ "recipesView.copyDeeplinkFailedMsg": {
+ "defaultMessage": "Failed to copy deeplink to clipboard"
+ },
+ "recipesView.copyFailedTitle": {
+ "defaultMessage": "Copy failed"
+ },
+ "recipesView.copyYaml": {
+ "defaultMessage": "Copy YAML"
+ },
+ "recipesView.copyYamlFailedMsg": {
+ "defaultMessage": "Failed to copy recipe YAML to clipboard"
+ },
+ "recipesView.createRecipe": {
+ "defaultMessage": "Create Recipe"
+ },
+ "recipesView.deeplinkCopiedMsg": {
+ "defaultMessage": "Recipe deeplink has been copied to clipboard"
+ },
+ "recipesView.deeplinkCopiedTitle": {
+ "defaultMessage": "Deeplink copied"
+ },
+ "recipesView.deleteRecipe": {
+ "defaultMessage": "Delete recipe"
+ },
+ "recipesView.deleteRecipeConfirm": {
+ "defaultMessage": "Are you sure you want to delete \"{title}\"?"
+ },
+ "recipesView.deleteRecipeDetail": {
+ "defaultMessage": "Recipe file will be deleted."
+ },
+ "recipesView.deleteRecipeTitle": {
+ "defaultMessage": "Delete Recipe"
+ },
+ "recipesView.editRecipe": {
+ "defaultMessage": "Edit recipe"
+ },
+ "recipesView.editSchedule": {
+ "defaultMessage": "Edit schedule"
+ },
+ "recipesView.editSlashCommand": {
+ "defaultMessage": "Edit slash command"
+ },
+ "recipesView.errorLoadingRecipes": {
+ "defaultMessage": "Error Loading Recipes"
+ },
+ "recipesView.exportFailedMsg": {
+ "defaultMessage": "Failed to export recipe to file"
+ },
+ "recipesView.exportFailedTitle": {
+ "defaultMessage": "Export failed"
+ },
+ "recipesView.exportRecipeDialogTitle": {
+ "defaultMessage": "Export Recipe"
+ },
+ "recipesView.exportToFile": {
+ "defaultMessage": "Export to File"
+ },
+ "recipesView.noMatchingRecipes": {
+ "defaultMessage": "No matching recipes found"
+ },
+ "recipesView.noSavedRecipes": {
+ "defaultMessage": "No saved recipes"
+ },
+ "recipesView.noSavedRecipesDescription": {
+ "defaultMessage": "Recipe saved from chats will show up here."
+ },
+ "recipesView.openInNewWindow": {
+ "defaultMessage": "Open in new window"
+ },
+ "recipesView.recipeDeletedSuccess": {
+ "defaultMessage": "Recipe deleted successfully"
+ },
+ "recipesView.recipeExportedMsg": {
+ "defaultMessage": "Recipe saved to {filePath}"
+ },
+ "recipesView.recipeExportedTitle": {
+ "defaultMessage": "Recipe exported"
+ },
+ "recipesView.recipesDescription": {
+ "defaultMessage": "View and manage your saved recipes to quickly start new sessions with predefined configurations. {shortcut} to search."
+ },
+ "recipesView.recipesTitle": {
+ "defaultMessage": "Recipes"
+ },
+ "recipesView.remove": {
+ "defaultMessage": "Remove"
+ },
+ "recipesView.removeSchedule": {
+ "defaultMessage": "Remove Schedule"
+ },
+ "recipesView.runs": {
+ "defaultMessage": "Runs {schedule}"
+ },
+ "recipesView.save": {
+ "defaultMessage": "Save"
+ },
+ "recipesView.scheduleDialogTitle": {
+ "defaultMessage": "{action} Schedule"
+ },
+ "recipesView.scheduleRemovedMsg": {
+ "defaultMessage": "Recipe will no longer run automatically"
+ },
+ "recipesView.scheduleRemovedTitle": {
+ "defaultMessage": "Schedule removed"
+ },
+ "recipesView.scheduleSavedMsg": {
+ "defaultMessage": "Recipe will run {schedule}"
+ },
+ "recipesView.scheduleSavedTitle": {
+ "defaultMessage": "Schedule saved"
+ },
+ "recipesView.searchRecipesPlaceholder": {
+ "defaultMessage": "Search recipes..."
+ },
+ "recipesView.shareRecipe": {
+ "defaultMessage": "Share recipe"
+ },
+ "recipesView.slashCommandDescription": {
+ "defaultMessage": "Set a slash command to quickly run this recipe from any chat"
+ },
+ "recipesView.slashCommandPlaceholder": {
+ "defaultMessage": "command-name"
+ },
+ "recipesView.slashCommandRemovedMsg": {
+ "defaultMessage": "Recipe slash command has been removed"
+ },
+ "recipesView.slashCommandRemovedTitle": {
+ "defaultMessage": "Slash command removed"
+ },
+ "recipesView.slashCommandSavedMsg": {
+ "defaultMessage": "Use /{command} to run this recipe"
+ },
+ "recipesView.slashCommandSavedTitle": {
+ "defaultMessage": "Slash command saved"
+ },
+ "recipesView.slashCommandTitle": {
+ "defaultMessage": "Slash Command"
+ },
+ "recipesView.slashCommandUsageHint": {
+ "defaultMessage": "Use /{command} in any chat to run this recipe"
+ },
+ "recipesView.tryAgain": {
+ "defaultMessage": "Try Again"
+ },
+ "recipesView.useRecipe": {
+ "defaultMessage": "Use recipe"
+ },
+ "recipesView.yamlCopiedMsg": {
+ "defaultMessage": "Recipe YAML has been copied to clipboard"
+ },
+ "recipesView.yamlCopiedTitle": {
+ "defaultMessage": "YAML copied"
+ },
+ "recipesView.yamlFiles": {
+ "defaultMessage": "YAML Files"
+ },
+ "resetProviderSection.resetButton": {
+ "defaultMessage": "Reset Provider and Model"
+ },
+ "resetProviderSection.resetDescription": {
+ "defaultMessage": "This will clear your selected model and provider settings. If no defaults are available, you'll be taken to the welcome screen to set them up again."
+ },
+ "responseStyle.conciseDescription": {
+ "defaultMessage": "Tool calls are by default closed and only show the tool used"
+ },
+ "responseStyle.conciseLabel": {
+ "defaultMessage": "Concise"
+ },
+ "responseStyle.detailedDescription": {
+ "defaultMessage": "Tool calls are by default shown open to expose details"
+ },
+ "responseStyle.detailedLabel": {
+ "defaultMessage": "Detailed"
+ },
+ "scheduleDetailView.actions": {
+ "defaultMessage": "Actions"
+ },
+ "scheduleDetailView.cannotModifyRunning": {
+ "defaultMessage": "Cannot trigger or modify a schedule while it's already running."
+ },
+ "scheduleDetailView.created": {
+ "defaultMessage": "Created: {date}"
+ },
+ "scheduleDetailView.cronExpression": {
+ "defaultMessage": "Cron Expression:"
+ },
+ "scheduleDetailView.currentSession": {
+ "defaultMessage": "Current Session:"
+ },
+ "scheduleDetailView.currentlyRunning": {
+ "defaultMessage": "Currently Running"
+ },
+ "scheduleDetailView.dir": {
+ "defaultMessage": "Dir: {path}"
+ },
+ "scheduleDetailView.editSchedule": {
+ "defaultMessage": "Edit Schedule"
+ },
+ "scheduleDetailView.errorPrefix": {
+ "defaultMessage": "Error: {error}"
+ },
+ "scheduleDetailView.failedToLoadSession": {
+ "defaultMessage": "Failed to load session"
+ },
+ "scheduleDetailView.idLabel": {
+ "defaultMessage": "ID:"
+ },
+ "scheduleDetailView.inspectJobError": {
+ "defaultMessage": "Inspect Job Error"
+ },
+ "scheduleDetailView.inspectNoInfo": {
+ "defaultMessage": "No detailed information available"
+ },
+ "scheduleDetailView.inspectRunningJob": {
+ "defaultMessage": "Inspect Running Job"
+ },
+ "scheduleDetailView.jobCancelled": {
+ "defaultMessage": "Job Cancelled"
+ },
+ "scheduleDetailView.jobCancelledMsg": {
+ "defaultMessage": "The job was cancelled while starting up."
+ },
+ "scheduleDetailView.jobInspection": {
+ "defaultMessage": "Job Inspection"
+ },
+ "scheduleDetailView.jobKilled": {
+ "defaultMessage": "Job Killed"
+ },
+ "scheduleDetailView.killJobError": {
+ "defaultMessage": "Kill Job Error"
+ },
+ "scheduleDetailView.killRunningJob": {
+ "defaultMessage": "Kill Running Job"
+ },
+ "scheduleDetailView.lastRun": {
+ "defaultMessage": "Last Run:"
+ },
+ "scheduleDetailView.loadingSchedule": {
+ "defaultMessage": "Loading schedule..."
+ },
+ "scheduleDetailView.loadingSessions": {
+ "defaultMessage": "Loading sessions..."
+ },
+ "scheduleDetailView.messages": {
+ "defaultMessage": "Messages: {count}"
+ },
+ "scheduleDetailView.newSession": {
+ "defaultMessage": "New session: {sessionId}"
+ },
+ "scheduleDetailView.noScheduleId": {
+ "defaultMessage": "No schedule ID provided. Return to schedules list."
+ },
+ "scheduleDetailView.noSessions": {
+ "defaultMessage": "No sessions found for this schedule."
+ },
+ "scheduleDetailView.pauseSchedule": {
+ "defaultMessage": "Pause Schedule"
+ },
+ "scheduleDetailView.pauseUnpauseError": {
+ "defaultMessage": "Pause/Unpause Error"
+ },
+ "scheduleDetailView.paused": {
+ "defaultMessage": "Paused"
+ },
+ "scheduleDetailView.pausedMsg": {
+ "defaultMessage": "Paused \"{id}\""
+ },
+ "scheduleDetailView.pausedWarning": {
+ "defaultMessage": "This schedule is paused and will not run automatically. Use \"Run Schedule Now\" to trigger it manually or unpause to resume automatic execution."
+ },
+ "scheduleDetailView.processStarted": {
+ "defaultMessage": "Process Started:"
+ },
+ "scheduleDetailView.recentSessions": {
+ "defaultMessage": "Recent Sessions"
+ },
+ "scheduleDetailView.recipeSource": {
+ "defaultMessage": "Recipe Source:"
+ },
+ "scheduleDetailView.runScheduleError": {
+ "defaultMessage": "Run Schedule Error"
+ },
+ "scheduleDetailView.runScheduleNow": {
+ "defaultMessage": "Run Schedule Now"
+ },
+ "scheduleDetailView.scheduleDetails": {
+ "defaultMessage": "Schedule Details"
+ },
+ "scheduleDetailView.scheduleInformation": {
+ "defaultMessage": "Schedule Information"
+ },
+ "scheduleDetailView.scheduleLabel": {
+ "defaultMessage": "Schedule:"
+ },
+ "scheduleDetailView.scheduleNotFound": {
+ "defaultMessage": "Schedule Not Found"
+ },
+ "scheduleDetailView.scheduleNotFoundError": {
+ "defaultMessage": "Schedule not found"
+ },
+ "scheduleDetailView.schedulePaused": {
+ "defaultMessage": "Schedule Paused"
+ },
+ "scheduleDetailView.scheduleTriggered": {
+ "defaultMessage": "Schedule Triggered"
+ },
+ "scheduleDetailView.scheduleUnpaused": {
+ "defaultMessage": "Schedule Unpaused"
+ },
+ "scheduleDetailView.scheduleUpdated": {
+ "defaultMessage": "Schedule Updated"
+ },
+ "scheduleDetailView.sessionId": {
+ "defaultMessage": "Session ID: {id}"
+ },
+ "scheduleDetailView.tokens": {
+ "defaultMessage": "Tokens: {count}"
+ },
+ "scheduleDetailView.unpauseSchedule": {
+ "defaultMessage": "Unpause Schedule"
+ },
+ "scheduleDetailView.unpausedMsg": {
+ "defaultMessage": "Unpaused \"{id}\""
+ },
+ "scheduleDetailView.updateScheduleError": {
+ "defaultMessage": "Update Schedule Error"
+ },
+ "scheduleDetailView.updatedMsg": {
+ "defaultMessage": "Updated \"{id}\""
+ },
+ "scheduleDetailView.viewingScheduleId": {
+ "defaultMessage": "Viewing Schedule ID: {id}"
+ },
+ "scheduleModal.browseYaml": {
+ "defaultMessage": "Browse for YAML file..."
+ },
+ "scheduleModal.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "scheduleModal.createNewSchedule": {
+ "defaultMessage": "Create New Schedule"
+ },
+ "scheduleModal.createSchedule": {
+ "defaultMessage": "Create Schedule"
+ },
+ "scheduleModal.creating": {
+ "defaultMessage": "Creating..."
+ },
+ "scheduleModal.deepLink": {
+ "defaultMessage": "Deep link"
+ },
+ "scheduleModal.deepLinkPlaceholder": {
+ "defaultMessage": "Paste goose://recipe link here..."
+ },
+ "scheduleModal.editSchedule": {
+ "defaultMessage": "Edit Schedule"
+ },
+ "scheduleModal.failedParseRecipe": {
+ "defaultMessage": "Failed to parse recipe from file."
+ },
+ "scheduleModal.failedReadFile": {
+ "defaultMessage": "Failed to read the selected file."
+ },
+ "scheduleModal.invalidDeepLink": {
+ "defaultMessage": "Invalid deep link. Please use a goose://recipe link."
+ },
+ "scheduleModal.invalidFileType": {
+ "defaultMessage": "Invalid file type: Please select a YAML file (.yaml or .yml)"
+ },
+ "scheduleModal.nameLabel": {
+ "defaultMessage": "Name:"
+ },
+ "scheduleModal.namePlaceholder": {
+ "defaultMessage": "e.g., daily-summary-job"
+ },
+ "scheduleModal.provideValidRecipe": {
+ "defaultMessage": "Please provide a valid recipe source."
+ },
+ "scheduleModal.recipeDescription": {
+ "defaultMessage": "Description: {description}"
+ },
+ "scheduleModal.recipeParsed": {
+ "defaultMessage": "Recipe parsed successfully"
+ },
+ "scheduleModal.recipeTitle": {
+ "defaultMessage": "Title: {title}"
+ },
+ "scheduleModal.scheduleIdRequired": {
+ "defaultMessage": "Schedule ID is required."
+ },
+ "scheduleModal.scheduleLabel": {
+ "defaultMessage": "Schedule:"
+ },
+ "scheduleModal.selected": {
+ "defaultMessage": "Selected: {path}"
+ },
+ "scheduleModal.sourceLabel": {
+ "defaultMessage": "Source:"
+ },
+ "scheduleModal.updateSchedule": {
+ "defaultMessage": "Update Schedule"
+ },
+ "scheduleModal.updating": {
+ "defaultMessage": "Updating..."
+ },
+ "scheduleModal.yaml": {
+ "defaultMessage": "YAML"
+ },
+ "schedulesView.confirmDelete": {
+ "defaultMessage": "Are you sure you want to delete schedule \"{id}\"?"
+ },
+ "schedulesView.createSchedule": {
+ "defaultMessage": "Create Schedule"
+ },
+ "schedulesView.description": {
+ "defaultMessage": "Create and manage scheduled tasks to run recipes automatically at specified times."
+ },
+ "schedulesView.edit": {
+ "defaultMessage": "Edit"
+ },
+ "schedulesView.errorPrefix": {
+ "defaultMessage": "Error: {error}"
+ },
+ "schedulesView.inspect": {
+ "defaultMessage": "Inspect"
+ },
+ "schedulesView.inspectError": {
+ "defaultMessage": "Inspect Job Error"
+ },
+ "schedulesView.inspectNoInfo": {
+ "defaultMessage": "No detailed information available for this job"
+ },
+ "schedulesView.jobInspection": {
+ "defaultMessage": "Job Inspection"
+ },
+ "schedulesView.jobKilled": {
+ "defaultMessage": "Job Killed"
+ },
+ "schedulesView.kill": {
+ "defaultMessage": "Kill"
+ },
+ "schedulesView.killError": {
+ "defaultMessage": "Kill Job Error"
+ },
+ "schedulesView.lastRun": {
+ "defaultMessage": "Last run: {date}"
+ },
+ "schedulesView.noSchedules": {
+ "defaultMessage": "No schedules yet"
+ },
+ "schedulesView.pause": {
+ "defaultMessage": "Pause"
+ },
+ "schedulesView.pauseError": {
+ "defaultMessage": "Pause Schedule Error"
+ },
+ "schedulesView.paused": {
+ "defaultMessage": "Paused"
+ },
+ "schedulesView.refresh": {
+ "defaultMessage": "Refresh"
+ },
+ "schedulesView.refreshing": {
+ "defaultMessage": "Refreshing..."
+ },
+ "schedulesView.resume": {
+ "defaultMessage": "Resume"
+ },
+ "schedulesView.running": {
+ "defaultMessage": "Running"
+ },
+ "schedulesView.schedulePaused": {
+ "defaultMessage": "Schedule Paused"
+ },
+ "schedulesView.schedulePausedMsg": {
+ "defaultMessage": "Successfully paused schedule \"{id}\""
+ },
+ "schedulesView.scheduleUnpaused": {
+ "defaultMessage": "Schedule Unpaused"
+ },
+ "schedulesView.scheduleUnpausedMsg": {
+ "defaultMessage": "Successfully unpaused schedule \"{id}\""
+ },
+ "schedulesView.scheduleUpdated": {
+ "defaultMessage": "Schedule Updated"
+ },
+ "schedulesView.scheduleUpdatedMsg": {
+ "defaultMessage": "Successfully updated schedule \"{id}\""
+ },
+ "schedulesView.scheduler": {
+ "defaultMessage": "Scheduler"
+ },
+ "schedulesView.unpauseError": {
+ "defaultMessage": "Unpause Schedule Error"
+ },
+ "searchBar.caseSensitive": {
+ "defaultMessage": "Case Sensitive"
+ },
+ "searchBar.close": {
+ "defaultMessage": "Close ({shortcut})"
+ },
+ "searchBar.next": {
+ "defaultMessage": "Next ({shortcut})"
+ },
+ "searchBar.placeholder": {
+ "defaultMessage": "Search conversation..."
+ },
+ "searchBar.previous": {
+ "defaultMessage": "Previous ({shortcut})"
+ },
+ "secureStorageNotice.defaultMessage": {
+ "defaultMessage": "Keys are stored securely in the keychain"
+ },
+ "securityToggle.apiTokenDescription": {
+ "defaultMessage": "Authentication token for the classification service"
+ },
+ "securityToggle.apiTokenOptional": {
+ "defaultMessage": "API Token (Optional)"
+ },
+ "securityToggle.classificationEndpoint": {
+ "defaultMessage": "Classification Endpoint"
+ },
+ "securityToggle.classificationEndpointDescription": {
+ "defaultMessage": "Enter the full URL for your classification service"
+ },
+ "securityToggle.commandClassifierActive": {
+ "defaultMessage": "Command classifier active (auto-configured from environment)"
+ },
+ "securityToggle.commandEndpointDescription": {
+ "defaultMessage": "Enter the full URL for your command injection classification service"
+ },
+ "securityToggle.commandInjectionDescription": {
+ "defaultMessage": "Use ML models to detect malicious shell commands"
+ },
+ "securityToggle.detectionModel": {
+ "defaultMessage": "Detection Model"
+ },
+ "securityToggle.detectionModelDescription": {
+ "defaultMessage": "Select which ML model to use for prompt injection detection"
+ },
+ "securityToggle.detectionThreshold": {
+ "defaultMessage": "Detection Threshold"
+ },
+ "securityToggle.enableCommandInjection": {
+ "defaultMessage": "Enable Command Injection ML Detection"
+ },
+ "securityToggle.enablePromptInjection": {
+ "defaultMessage": "Enable Prompt Injection Detection"
+ },
+ "securityToggle.enablePromptInjectionMl": {
+ "defaultMessage": "Enable Prompt Injection ML Detection"
+ },
+ "securityToggle.mlEndpointDescription": {
+ "defaultMessage": "Enter the full URL for your ML classification service (including model identifier)"
+ },
+ "securityToggle.mlTokenDescription": {
+ "defaultMessage": "Authentication token for the ML service (e.g., HuggingFace token)"
+ },
+ "securityToggle.promptInjectionDescription": {
+ "defaultMessage": "Detect and prevent potential prompt injection attacks"
+ },
+ "securityToggle.promptInjectionMlDescription": {
+ "defaultMessage": "Use ML models to detect potential prompt injection in your chat"
+ },
+ "securityToggle.thresholdDescription": {
+ "defaultMessage": "Higher values are more strict (0.01 = very lenient, 1.0 = maximum strict)"
+ },
+ "sessionHistory.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "sessionHistory.copy": {
+ "defaultMessage": "Copy"
+ },
+ "sessionHistory.empty.description": {
+ "defaultMessage": "This session doesn't contain any messages"
+ },
+ "sessionHistory.empty.title": {
+ "defaultMessage": "No messages found"
+ },
+ "sessionHistory.error.loading": {
+ "defaultMessage": "Error Loading Session Details"
+ },
+ "sessionHistory.error.tryAgain": {
+ "defaultMessage": "Try Again"
+ },
+ "sessionHistory.loading": {
+ "defaultMessage": "Loading session details..."
+ },
+ "sessionHistory.resume": {
+ "defaultMessage": "Resume"
+ },
+ "sessionHistory.searchPlaceholder": {
+ "defaultMessage": "Search history..."
+ },
+ "sessionHistory.share": {
+ "defaultMessage": "Share"
+ },
+ "sessionHistory.shareModal.description": {
+ "defaultMessage": "Share this session link to give others a read only view of your goose chat."
+ },
+ "sessionHistory.shareModal.title": {
+ "defaultMessage": "Share Session (beta)"
+ },
+ "sessionHistory.shareTooltip": {
+ "defaultMessage": "To enable session sharing, go to Settings > Session > Session Sharing ."
+ },
+ "sessionHistory.sharing": {
+ "defaultMessage": "Sharing..."
+ },
+ "sessionHistory.toast.copyFailed": {
+ "defaultMessage": "Failed to copy link to clipboard"
+ },
+ "sessionHistory.toast.launchFailed": {
+ "defaultMessage": "Could not launch session: {error}"
+ },
+ "sessionHistory.toast.shareFailed": {
+ "defaultMessage": "Failed to share session: {error}"
+ },
+ "sessionIndicators.error": {
+ "defaultMessage": "Session encountered an error"
+ },
+ "sessionIndicators.newActivity": {
+ "defaultMessage": "Has new activity"
+ },
+ "sessionIndicators.streaming": {
+ "defaultMessage": "Streaming"
+ },
+ "sessionItem.messageCount": {
+ "defaultMessage": "{count} messages"
+ },
+ "sessionSharingSection.alreadyConfigured": {
+ "defaultMessage": "Session sharing has already been configured"
+ },
+ "sessionSharingSection.baseUrl": {
+ "defaultMessage": "Base URL"
+ },
+ "sessionSharingSection.connectionFailed": {
+ "defaultMessage": "Connection failed."
+ },
+ "sessionSharingSection.connectionSuccess": {
+ "defaultMessage": "Connection successful!"
+ },
+ "sessionSharingSection.connectionTimedOut": {
+ "defaultMessage": "Connection timed out. The server may be slow or unreachable."
+ },
+ "sessionSharingSection.descriptionConfigured": {
+ "defaultMessage": "Session sharing is configured but fully opt-in — your sessions are only shared when you explicitly click the share button."
+ },
+ "sessionSharingSection.descriptionDefault": {
+ "defaultMessage": "You can enable session sharing to share your sessions with others."
+ },
+ "sessionSharingSection.enableSharing": {
+ "defaultMessage": "Enable session sharing"
+ },
+ "sessionSharingSection.invalidUrl": {
+ "defaultMessage": "Invalid URL format. Please enter a valid URL (e.g. https://example.com/api)."
+ },
+ "sessionSharingSection.serverError": {
+ "defaultMessage": "Server error: HTTP {status}. The server may not be configured correctly."
+ },
+ "sessionSharingSection.testConnection": {
+ "defaultMessage": "Test Connection"
+ },
+ "sessionSharingSection.testing": {
+ "defaultMessage": "Testing..."
+ },
+ "sessionSharingSection.testingConnection": {
+ "defaultMessage": "Testing connection..."
+ },
+ "sessionSharingSection.title": {
+ "defaultMessage": "Session Sharing"
+ },
+ "sessionSharingSection.unknownError": {
+ "defaultMessage": "Unknown error occurred."
+ },
+ "sessionSharingSection.unreachableServer": {
+ "defaultMessage": "Unable to reach the server. Please check the URL and your network connection."
+ },
+ "sessionSharingSection.urlPlaceholder": {
+ "defaultMessage": "https://example.com/api"
+ },
+ "sessionViewComponents.empty.description": {
+ "defaultMessage": "This session doesn't contain any messages"
+ },
+ "sessionViewComponents.empty.title": {
+ "defaultMessage": "No messages found"
+ },
+ "sessionViewComponents.error.loading": {
+ "defaultMessage": "Error Loading Session Details"
+ },
+ "sessionViewComponents.error.tryAgain": {
+ "defaultMessage": "Try Again"
+ },
+ "sessionViewComponents.role.assistant": {
+ "defaultMessage": "Goose"
+ },
+ "sessionViewComponents.role.user": {
+ "defaultMessage": "You"
+ },
+ "sessions.action.delete": {
+ "defaultMessage": "Delete session"
+ },
+ "sessions.action.duplicate": {
+ "defaultMessage": "Duplicate session"
+ },
+ "sessions.action.editName": {
+ "defaultMessage": "Edit session name"
+ },
+ "sessions.action.export": {
+ "defaultMessage": "Export session"
+ },
+ "sessions.action.openNewWindow": {
+ "defaultMessage": "Open in new window"
+ },
+ "sessions.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "sessions.chatHistory": {
+ "defaultMessage": "Chat history"
+ },
+ "sessions.chatHistoryDesc": {
+ "defaultMessage": "View and search your past conversations with Goose. {shortcut} to search."
+ },
+ "sessions.delete.message": {
+ "defaultMessage": "Are you sure you want to delete the session \"{name}\"? This action cannot be undone."
+ },
+ "sessions.delete.title": {
+ "defaultMessage": "Delete Session"
+ },
+ "sessions.edit.placeholder": {
+ "defaultMessage": "Enter session description"
+ },
+ "sessions.edit.title": {
+ "defaultMessage": "Edit Session Description"
+ },
+ "sessions.empty.description": {
+ "defaultMessage": "Your chat history will appear here"
+ },
+ "sessions.empty.title": {
+ "defaultMessage": "No chat sessions found"
+ },
+ "sessions.error.loading": {
+ "defaultMessage": "Error Loading Sessions"
+ },
+ "sessions.error.tryAgain": {
+ "defaultMessage": "Try Again"
+ },
+ "sessions.extensions": {
+ "defaultMessage": "Extensions:"
+ },
+ "sessions.import": {
+ "defaultMessage": "Import Session"
+ },
+ "sessions.loadingMore": {
+ "defaultMessage": "Loading more sessions..."
+ },
+ "sessions.save": {
+ "defaultMessage": "Save"
+ },
+ "sessions.saving": {
+ "defaultMessage": "Saving..."
+ },
+ "sessions.search.noResults": {
+ "defaultMessage": "No matching sessions found"
+ },
+ "sessions.search.noResultsDesc": {
+ "defaultMessage": "Try adjusting your search terms"
+ },
+ "sessions.searchPlaceholder": {
+ "defaultMessage": "Search history..."
+ },
+ "sessions.toast.deleteFailed": {
+ "defaultMessage": "Failed to delete session \"{name}\": {error}"
+ },
+ "sessions.toast.deleted": {
+ "defaultMessage": "Session deleted successfully"
+ },
+ "sessions.toast.duplicateFailed": {
+ "defaultMessage": "Failed to duplicate session: {error}"
+ },
+ "sessions.toast.duplicated": {
+ "defaultMessage": "Session \"{name}\" duplicated successfully"
+ },
+ "sessions.toast.exported": {
+ "defaultMessage": "Session exported successfully"
+ },
+ "sessions.toast.importFailed": {
+ "defaultMessage": "Failed to import session: {error}"
+ },
+ "sessions.toast.imported": {
+ "defaultMessage": "Session imported successfully"
+ },
+ "sessions.toast.updateFailed": {
+ "defaultMessage": "Failed to update session description: {error}"
+ },
+ "sessions.toast.updated": {
+ "defaultMessage": "Session description updated successfully"
+ },
+ "sessionsInsights.failedToLoad": {
+ "defaultMessage": "Failed to load insights"
+ },
+ "sessionsInsights.noRecentChats": {
+ "defaultMessage": "No recent chat sessions found."
+ },
+ "sessionsInsights.recentChats": {
+ "defaultMessage": "Recent chats"
+ },
+ "sessionsInsights.seeAll": {
+ "defaultMessage": "See all"
+ },
+ "sessionsInsights.totalSessions": {
+ "defaultMessage": "Total sessions"
+ },
+ "sessionsInsights.totalTokens": {
+ "defaultMessage": "Total tokens"
+ },
+ "sessionsList.showAll": {
+ "defaultMessage": "Show All"
+ },
+ "sessionsList.startNewChat": {
+ "defaultMessage": "Start New Chat"
+ },
+ "sessionsList.untitledSession": {
+ "defaultMessage": "Untitled session"
+ },
+ "sessionsView.error.failedToLoad": {
+ "defaultMessage": "Failed to load session details. Please try again later."
+ },
+ "sessionsView.loading": {
+ "defaultMessage": "Loading..."
+ },
+ "settings.appearance.description": {
+ "defaultMessage": "Configure how goose appears on your system"
+ },
+ "settings.appearance.title": {
+ "defaultMessage": "Appearance"
+ },
+ "settings.close": {
+ "defaultMessage": "Close"
+ },
+ "settings.costTracking.description": {
+ "defaultMessage": "Show model pricing and usage costs"
+ },
+ "settings.costTracking.title": {
+ "defaultMessage": "Cost Tracking"
+ },
+ "settings.dockIcon.description": {
+ "defaultMessage": "Show goose in the dock"
+ },
+ "settings.dockIcon.title": {
+ "defaultMessage": "Dock icon"
+ },
+ "settings.help.description": {
+ "defaultMessage": "Help us improve goose by reporting issues or requesting new features"
+ },
+ "settings.help.reportBug": {
+ "defaultMessage": "Report a Bug"
+ },
+ "settings.help.requestFeature": {
+ "defaultMessage": "Request a Feature"
+ },
+ "settings.help.title": {
+ "defaultMessage": "Help & feedback"
+ },
+ "settings.menuBarIcon.description": {
+ "defaultMessage": "Show goose in the menu bar"
+ },
+ "settings.menuBarIcon.title": {
+ "defaultMessage": "Menu bar icon"
+ },
+ "settings.navigation.customize": {
+ "defaultMessage": "Customize Items"
+ },
+ "settings.navigation.description": {
+ "defaultMessage": "Customize navigation layout and behavior"
+ },
+ "settings.navigation.mode": {
+ "defaultMessage": "Mode"
+ },
+ "settings.navigation.position": {
+ "defaultMessage": "Position"
+ },
+ "settings.navigation.style": {
+ "defaultMessage": "Style"
+ },
+ "settings.navigation.title": {
+ "defaultMessage": "Navigation"
+ },
+ "settings.notifications.configGuide": {
+ "defaultMessage": "Configuration guide"
+ },
+ "settings.notifications.description": {
+ "defaultMessage": "Notifications are managed by your OS - {link}"
+ },
+ "settings.notifications.modal.macInstructions": {
+ "defaultMessage": "To enable notifications on macOS:"
+ },
+ "settings.notifications.modal.macStep1": {
+ "defaultMessage": "Open System Preferences"
+ },
+ "settings.notifications.modal.macStep2": {
+ "defaultMessage": "Click on Notifications"
+ },
+ "settings.notifications.modal.macStep3": {
+ "defaultMessage": "Find and select goose in the application list"
+ },
+ "settings.notifications.modal.macStep4": {
+ "defaultMessage": "Enable notifications and adjust settings as desired"
+ },
+ "settings.notifications.modal.title": {
+ "defaultMessage": "How to Enable Notifications"
+ },
+ "settings.notifications.modal.winInstructions": {
+ "defaultMessage": "To enable notifications on Windows:"
+ },
+ "settings.notifications.modal.winStep1": {
+ "defaultMessage": "Open Settings"
+ },
+ "settings.notifications.modal.winStep2": {
+ "defaultMessage": "Go to System > Notifications"
+ },
+ "settings.notifications.modal.winStep3": {
+ "defaultMessage": "Find and select goose in the application list"
+ },
+ "settings.notifications.modal.winStep4": {
+ "defaultMessage": "Toggle notifications on and adjust settings as desired"
+ },
+ "settings.notifications.openSettings": {
+ "defaultMessage": "Open Settings"
+ },
+ "settings.notifications.title": {
+ "defaultMessage": "Notifications"
+ },
+ "settings.preventSleep.description": {
+ "defaultMessage": "Keep your computer awake while goose is running a task (screen can still lock)"
+ },
+ "settings.preventSleep.title": {
+ "defaultMessage": "Prevent Sleep"
+ },
+ "settings.theme.description": {
+ "defaultMessage": "Customize the look and feel of goose"
+ },
+ "settings.theme.title": {
+ "defaultMessage": "Theme"
+ },
+ "settings.updates.description": {
+ "defaultMessage": "Check for and install updates to keep goose running at its best"
+ },
+ "settings.updates.title": {
+ "defaultMessage": "Updates"
+ },
+ "settings.version.title": {
+ "defaultMessage": "Version"
+ },
+ "settingsView.tabApp": {
+ "defaultMessage": "App"
+ },
+ "settingsView.tabChat": {
+ "defaultMessage": "Chat"
+ },
+ "settingsView.tabKeyboard": {
+ "defaultMessage": "Keyboard"
+ },
+ "settingsView.tabLocalInference": {
+ "defaultMessage": "Local Inference"
+ },
+ "settingsView.tabModels": {
+ "defaultMessage": "Models"
+ },
+ "settingsView.tabPrompts": {
+ "defaultMessage": "Prompts"
+ },
+ "settingsView.tabSession": {
+ "defaultMessage": "Session"
+ },
+ "settingsView.title": {
+ "defaultMessage": "Settings"
+ },
+ "sharedSession.loading": {
+ "defaultMessage": "Loading session details..."
+ },
+ "sharedSession.title": {
+ "defaultMessage": "Shared Session"
+ },
+ "sheet.close": {
+ "defaultMessage": "Close"
+ },
+ "shortcutRecorder.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "shortcutRecorder.clickToRecord": {
+ "defaultMessage": "Click to record..."
+ },
+ "shortcutRecorder.conflictWarning": {
+ "defaultMessage": "This shortcut is already used by {label}. Saving will reassign it to this action."
+ },
+ "shortcutRecorder.pressShortcut": {
+ "defaultMessage": "Press shortcut..."
+ },
+ "shortcutRecorder.save": {
+ "defaultMessage": "Save"
+ },
+ "spellcheckToggle.description": {
+ "defaultMessage": "Check spelling in the chat input. Requires restart to take effect."
+ },
+ "spellcheckToggle.title": {
+ "defaultMessage": "Enable Spellcheck"
+ },
+ "standaloneAppView.failedToLoad": {
+ "defaultMessage": "Failed to Load App"
+ },
+ "standaloneAppView.initializing": {
+ "defaultMessage": "Initializing app..."
+ },
+ "standaloneAppView.missingParams": {
+ "defaultMessage": "Missing required parameters"
+ },
+ "stringUtils.configuredProvider": {
+ "defaultMessage": "{name} provider is configured"
+ },
+ "stringUtils.ollamaApp": {
+ "defaultMessage": "Ollama app"
+ },
+ "stringUtils.ollamaNotConfiguredPrefix": {
+ "defaultMessage": "To use, either the"
+ },
+ "stringUtils.ollamaNotConfiguredSuffix": {
+ "defaultMessage": "must be installed on your machine and open, or you must enter a value for OLLAMA_HOST."
+ },
+ "subRecipeEditor.addExisting": {
+ "defaultMessage": "Add Existing"
+ },
+ "subRecipeEditor.createNew": {
+ "defaultMessage": "Create New Subrecipe"
+ },
+ "subRecipeEditor.deleteSubrecipe": {
+ "defaultMessage": "Delete subrecipe {name}"
+ },
+ "subRecipeEditor.description": {
+ "defaultMessage": "Subrecipes are recipes that can be called as tools during execution. They enable multi-step workflows and reusable components."
+ },
+ "subRecipeEditor.duplicateName": {
+ "defaultMessage": "Duplicate Name"
+ },
+ "subRecipeEditor.duplicateNameMsg": {
+ "defaultMessage": "A subrecipe named \"{name}\" already exists. Please use a unique name."
+ },
+ "subRecipeEditor.editSubrecipe": {
+ "defaultMessage": "Edit subrecipe {name}"
+ },
+ "subRecipeEditor.label": {
+ "defaultMessage": "Subrecipes"
+ },
+ "subRecipeEditor.preconfiguredValues": {
+ "defaultMessage": "Pre-configured values:"
+ },
+ "subRecipeEditor.sequential": {
+ "defaultMessage": "Sequential"
+ },
+ "subRecipeModal.addTitle": {
+ "defaultMessage": "Add Subrecipe"
+ },
+ "subRecipeModal.apply": {
+ "defaultMessage": "Apply"
+ },
+ "subRecipeModal.browse": {
+ "defaultMessage": "Browse"
+ },
+ "subRecipeModal.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "subRecipeModal.closeModal": {
+ "defaultMessage": "Close subrecipe modal"
+ },
+ "subRecipeModal.configureTitle": {
+ "defaultMessage": "Configure Subrecipe"
+ },
+ "subRecipeModal.descriptionLabel": {
+ "defaultMessage": "Description"
+ },
+ "subRecipeModal.descriptionPlaceholder": {
+ "defaultMessage": "Optional description of what this subrecipe does..."
+ },
+ "subRecipeModal.invalidFile": {
+ "defaultMessage": "Invalid File"
+ },
+ "subRecipeModal.invalidFileMsg": {
+ "defaultMessage": "Please select a YAML file (.yaml or .yml)."
+ },
+ "subRecipeModal.nameHint": {
+ "defaultMessage": "Unique identifier used to generate the tool name"
+ },
+ "subRecipeModal.nameLabel": {
+ "defaultMessage": "Name"
+ },
+ "subRecipeModal.namePlaceholder": {
+ "defaultMessage": "e.g., security_scan"
+ },
+ "subRecipeModal.pathHint": {
+ "defaultMessage": "Browse for an existing recipe file or enter a path manually"
+ },
+ "subRecipeModal.pathLabel": {
+ "defaultMessage": "Path"
+ },
+ "subRecipeModal.pathPlaceholder": {
+ "defaultMessage": "e.g., ./subrecipes/security-analysis.yaml"
+ },
+ "subRecipeModal.preconfiguredValues": {
+ "defaultMessage": "Pre-configured Values"
+ },
+ "subRecipeModal.preconfiguredValuesHint": {
+ "defaultMessage": "Optional parameter values that are always passed to the subrecipe"
+ },
+ "subRecipeModal.sequentialHint": {
+ "defaultMessage": "(Forces sequential execution of multiple subrecipe instances)"
+ },
+ "subRecipeModal.sequentialLabel": {
+ "defaultMessage": "Sequential when repeated"
+ },
+ "subRecipeModal.subtitle": {
+ "defaultMessage": "Configure a subrecipe that can be called as a tool during recipe execution"
+ },
+ "switchModelModal.backToModelList": {
+ "defaultMessage": "Back to model list"
+ },
+ "switchModelModal.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "switchModelModal.checkProviderConfig": {
+ "defaultMessage": "Check your provider configuration in Settings → Providers"
+ },
+ "switchModelModal.chooseModel": {
+ "defaultMessage": "Choose a model:"
+ },
+ "switchModelModal.claudeAdaptive": {
+ "defaultMessage": "Adaptive - Claude decides when and how much to think"
+ },
+ "switchModelModal.claudeDisabled": {
+ "defaultMessage": "Disabled - No extended thinking"
+ },
+ "switchModelModal.claudeEffortHigh": {
+ "defaultMessage": "High - Deep reasoning (default)"
+ },
+ "switchModelModal.claudeEffortLow": {
+ "defaultMessage": "Low - Minimal thinking, fastest responses"
+ },
+ "switchModelModal.claudeEffortMax": {
+ "defaultMessage": "Max - No constraints on thinking depth"
+ },
+ "switchModelModal.claudeEffortMedium": {
+ "defaultMessage": "Medium - Moderate thinking"
+ },
+ "switchModelModal.claudeEnabled": {
+ "defaultMessage": "Enabled - Fixed token budget for thinking"
+ },
+ "switchModelModal.couldNotContactProvider": {
+ "defaultMessage": "Could not contact provider"
+ },
+ "switchModelModal.customModelName": {
+ "defaultMessage": "Custom model name"
+ },
+ "switchModelModal.description": {
+ "defaultMessage": "Select a provider and model to use for your conversations."
+ },
+ "switchModelModal.enterModelNotListed": {
+ "defaultMessage": "Enter a model not listed..."
+ },
+ "switchModelModal.extendedThinking": {
+ "defaultMessage": "Extended Thinking"
+ },
+ "switchModelModal.geminiOnly": {
+ "defaultMessage": "(Gemini 3 models only)"
+ },
+ "switchModelModal.goToSettings": {
+ "defaultMessage": "Go to Settings"
+ },
+ "switchModelModal.loadingModels": {
+ "defaultMessage": "Loading models…"
+ },
+ "switchModelModal.localModelsDescription": {
+ "defaultMessage": "To use local inference, you need to download a model to your computer first. Go to Settings → Models to manage local models."
+ },
+ "switchModelModal.localModelsTitle": {
+ "defaultMessage": "Local models need to be downloaded first"
+ },
+ "switchModelModal.providerPlaceholder": {
+ "defaultMessage": "Provider, type to search"
+ },
+ "switchModelModal.quickStartGuide": {
+ "defaultMessage": "Quick start guide"
+ },
+ "switchModelModal.recommended": {
+ "defaultMessage": "Recommended"
+ },
+ "switchModelModal.selectEffortLevel": {
+ "defaultMessage": "Select effort level"
+ },
+ "switchModelModal.selectModel": {
+ "defaultMessage": "Please select a model"
+ },
+ "switchModelModal.selectModelButton": {
+ "defaultMessage": "Select model"
+ },
+ "switchModelModal.selectModelPlaceholder": {
+ "defaultMessage": "Select a model, type to search"
+ },
+ "switchModelModal.selectOrEnterModel": {
+ "defaultMessage": "Please select or enter a model"
+ },
+ "switchModelModal.selectProvider": {
+ "defaultMessage": "Please select a provider"
+ },
+ "switchModelModal.selectThinkingLevel": {
+ "defaultMessage": "Select thinking level"
+ },
+ "switchModelModal.selectThinkingMode": {
+ "defaultMessage": "Select thinking mode"
+ },
+ "switchModelModal.thinkingBudget": {
+ "defaultMessage": "Thinking Budget (tokens)"
+ },
+ "switchModelModal.thinkingEffort": {
+ "defaultMessage": "Thinking Effort"
+ },
+ "switchModelModal.thinkingLevel": {
+ "defaultMessage": "Thinking Level"
+ },
+ "switchModelModal.thinkingLevelHigh": {
+ "defaultMessage": "High - Deeper reasoning, higher latency"
+ },
+ "switchModelModal.thinkingLevelLow": {
+ "defaultMessage": "Low - Better latency, lighter reasoning"
+ },
+ "switchModelModal.title": {
+ "defaultMessage": "Switch models"
+ },
+ "switchModelModal.typeModelName": {
+ "defaultMessage": "Type model name here"
+ },
+ "switchModelModal.useOtherProvider": {
+ "defaultMessage": "Use other provider"
+ },
+ "telemetryOptOutModal.collectErrors": {
+ "defaultMessage": "Error types (e.g., \"rate_limit\", \"auth\" - no details)"
+ },
+ "telemetryOptOutModal.collectExtensions": {
+ "defaultMessage": "Extensions and tool usage counts (names only)"
+ },
+ "telemetryOptOutModal.collectOs": {
+ "defaultMessage": "Operating system, version, and architecture"
+ },
+ "telemetryOptOutModal.collectProvider": {
+ "defaultMessage": "Provider and model used"
+ },
+ "telemetryOptOutModal.collectSession": {
+ "defaultMessage": "Session metrics (duration, interaction count, token usage)"
+ },
+ "telemetryOptOutModal.collectVersion": {
+ "defaultMessage": "goose version and install method"
+ },
+ "telemetryOptOutModal.configError": {
+ "defaultMessage": "Configuration Error"
+ },
+ "telemetryOptOutModal.configErrorMessage": {
+ "defaultMessage": "Failed to check telemetry configuration."
+ },
+ "telemetryOptOutModal.description": {
+ "defaultMessage": "Would you like to help improve goose by sharing anonymous usage data? This helps us understand how goose is used and identify areas for improvement."
+ },
+ "telemetryOptOutModal.heading": {
+ "defaultMessage": "Help improve goose"
+ },
+ "telemetryOptOutModal.optIn": {
+ "defaultMessage": "Yes, share anonymous usage data"
+ },
+ "telemetryOptOutModal.optOut": {
+ "defaultMessage": "No thanks"
+ },
+ "telemetryOptOutModal.privacyNote": {
+ "defaultMessage": "We never collect your conversations, code, tool arguments, error messages, or any personal data. You can change this setting anytime in Settings → App."
+ },
+ "telemetryOptOutModal.whatWeCollect": {
+ "defaultMessage": "What we collect:"
+ },
+ "telemetrySettings.configErrorTitle": {
+ "defaultMessage": "Configuration Error"
+ },
+ "telemetrySettings.description": {
+ "defaultMessage": "Control how your data is used"
+ },
+ "telemetrySettings.learnMore": {
+ "defaultMessage": "Learn more"
+ },
+ "telemetrySettings.loadError": {
+ "defaultMessage": "Failed to load telemetry settings."
+ },
+ "telemetrySettings.title": {
+ "defaultMessage": "Privacy"
+ },
+ "telemetrySettings.toggleDescription": {
+ "defaultMessage": "Help improve goose by sharing anonymous usage statistics."
+ },
+ "telemetrySettings.toggleLabel": {
+ "defaultMessage": "Anonymous usage data"
+ },
+ "telemetrySettings.updateError": {
+ "defaultMessage": "Failed to update telemetry settings."
+ },
+ "themeSelector.dark": {
+ "defaultMessage": "Dark"
+ },
+ "themeSelector.light": {
+ "defaultMessage": "Light"
+ },
+ "themeSelector.system": {
+ "defaultMessage": "System"
+ },
+ "themeSelector.theme": {
+ "defaultMessage": "Theme"
+ },
+ "toolApprovalButtons.allowOnce": {
+ "defaultMessage": "Allow Once"
+ },
+ "toolApprovalButtons.allowedOnce": {
+ "defaultMessage": "Allowed once"
+ },
+ "toolApprovalButtons.alwaysAllow": {
+ "defaultMessage": "Always Allow"
+ },
+ "toolApprovalButtons.alwaysAllowed": {
+ "defaultMessage": "Always allowed"
+ },
+ "toolApprovalButtons.cancelled": {
+ "defaultMessage": "Cancelled"
+ },
+ "toolApprovalButtons.denied": {
+ "defaultMessage": "Denied"
+ },
+ "toolApprovalButtons.deniedOnce": {
+ "defaultMessage": "Denied once"
+ },
+ "toolApprovalButtons.deny": {
+ "defaultMessage": "Deny"
+ },
+ "toolCallStatusIndicator.toolStatus": {
+ "defaultMessage": "Tool status: {status}"
+ },
+ "toolCallWithResponse.activityCount": {
+ "defaultMessage": "Activity ({count})"
+ },
+ "toolCallWithResponse.code": {
+ "defaultMessage": "Code"
+ },
+ "toolCallWithResponse.loadingSpinner": {
+ "defaultMessage": "Loading spinner"
+ },
+ "toolCallWithResponse.logs": {
+ "defaultMessage": "Logs"
+ },
+ "toolCallWithResponse.mcpUiExperimental": {
+ "defaultMessage": "MCP UI is experimental and may change at any time."
+ },
+ "toolCallWithResponse.output": {
+ "defaultMessage": "Output"
+ },
+ "toolCallWithResponse.toolDetails": {
+ "defaultMessage": "Tool Details"
+ },
+ "toolCallWithResponse.toolResultAlt": {
+ "defaultMessage": "Tool result"
+ },
+ "toolCallWithResponse.viewSubagentSession": {
+ "defaultMessage": "View subagent session"
+ },
+ "toolConfirmation.allowToolCall": {
+ "defaultMessage": "Do you allow this tool call?"
+ },
+ "toolConfirmation.gooseWouldLikeToCall": {
+ "defaultMessage": "Goose would like to call the above tool. Allow?"
+ },
+ "tunnelSection.appStoreQrInstructions": {
+ "defaultMessage": "Scan this QR code with your iPhone camera to install the goose mobile app from the App Store"
+ },
+ "tunnelSection.close": {
+ "defaultMessage": "Close"
+ },
+ "tunnelSection.connectionDetails": {
+ "defaultMessage": "Connection Details"
+ },
+ "tunnelSection.downloadIosApp": {
+ "defaultMessage": "Download goose iOS App"
+ },
+ "tunnelSection.failedToLoadStatus": {
+ "defaultMessage": "Failed to load tunnel status"
+ },
+ "tunnelSection.failedToStartTunnel": {
+ "defaultMessage": "Failed to start tunnel"
+ },
+ "tunnelSection.failedToStopTunnel": {
+ "defaultMessage": "Failed to stop tunnel"
+ },
+ "tunnelSection.getIosApp": {
+ "defaultMessage": "Get the iOS app"
+ },
+ "tunnelSection.mobileApp": {
+ "defaultMessage": "Mobile App"
+ },
+ "tunnelSection.mobileAppConnection": {
+ "defaultMessage": "Mobile App Connection"
+ },
+ "tunnelSection.openInAppStore": {
+ "defaultMessage": "Open in App Store"
+ },
+ "tunnelSection.or": {
+ "defaultMessage": "or"
+ },
+ "tunnelSection.previewDescription": {
+ "defaultMessage": "Enable remote access to goose from mobile devices using secure tunneling."
+ },
+ "tunnelSection.previewFeature": {
+ "defaultMessage": "Preview feature:"
+ },
+ "tunnelSection.qrCodeInstructions": {
+ "defaultMessage": "Scan this QR code with the goose mobile app. Do not share this code with anyone else as it is for your personal access."
+ },
+ "tunnelSection.retry": {
+ "defaultMessage": "Retry"
+ },
+ "tunnelSection.scanQrCode": {
+ "defaultMessage": "scan QR code"
+ },
+ "tunnelSection.secretKey": {
+ "defaultMessage": "Secret Key"
+ },
+ "tunnelSection.showQrCode": {
+ "defaultMessage": "Show QR Code"
+ },
+ "tunnelSection.startTunnel": {
+ "defaultMessage": "Start Tunnel"
+ },
+ "tunnelSection.starting": {
+ "defaultMessage": "Starting..."
+ },
+ "tunnelSection.statusDisabled": {
+ "defaultMessage": "Tunnel is disabled"
+ },
+ "tunnelSection.statusError": {
+ "defaultMessage": "Tunnel encountered an error"
+ },
+ "tunnelSection.statusIdle": {
+ "defaultMessage": "Tunnel is not running"
+ },
+ "tunnelSection.statusRunning": {
+ "defaultMessage": "Tunnel is active"
+ },
+ "tunnelSection.statusStarting": {
+ "defaultMessage": "Starting tunnel..."
+ },
+ "tunnelSection.stopTunnel": {
+ "defaultMessage": "Stop Tunnel"
+ },
+ "tunnelSection.tunnelStatus": {
+ "defaultMessage": "Tunnel Status"
+ },
+ "tunnelSection.tunnelUrl": {
+ "defaultMessage": "Tunnel URL"
+ },
+ "tunnelSection.url": {
+ "defaultMessage": "URL:"
+ },
+ "updateSection.autoDownload": {
+ "defaultMessage": "Update will be downloaded automatically in the background."
+ },
+ "updateSection.autoInstallNote": {
+ "defaultMessage": "The update will be installed automatically when you quit the app."
+ },
+ "updateSection.checkForUpdates": {
+ "defaultMessage": "Check for Updates"
+ },
+ "updateSection.checking": {
+ "defaultMessage": "Checking for updates..."
+ },
+ "updateSection.currentVersion": {
+ "defaultMessage": "Current version"
+ },
+ "updateSection.downloadReady": {
+ "defaultMessage": "Update downloaded and ready to install!"
+ },
+ "updateSection.downloadingProgress": {
+ "defaultMessage": "Downloading update... {percent}%"
+ },
+ "updateSection.downloadingUpdate": {
+ "defaultMessage": "Downloading update..."
+ },
+ "updateSection.installAndRestart": {
+ "defaultMessage": "Install & Restart"
+ },
+ "updateSection.installNowHint": {
+ "defaultMessage": "Or click \"Install & Restart\" to update now."
+ },
+ "updateSection.latestVersion": {
+ "defaultMessage": "You are running the latest version!"
+ },
+ "updateSection.loading": {
+ "defaultMessage": "Loading..."
+ },
+ "updateSection.manualInstallNote": {
+ "defaultMessage": "After download, you'll need to manually install the update."
+ },
+ "updateSection.manualInstallRequired": {
+ "defaultMessage": "Manual installation required for this update method."
+ },
+ "updateSection.readyInstallAuto": {
+ "defaultMessage": "✓ Update is ready! It will be installed when you quit Goose."
+ },
+ "updateSection.readyInstallManual": {
+ "defaultMessage": "✓ Update is ready! Click \"Install & Restart\" for installation instructions."
+ },
+ "updateSection.upToDate": {
+ "defaultMessage": "(up to date)"
+ },
+ "updateSection.updateAvailable": {
+ "defaultMessage": "Update available!"
+ },
+ "updateSection.versionAvailable": {
+ "defaultMessage": "→ {version} available"
+ },
+ "updateSection.versionIsAvailable": {
+ "defaultMessage": "Version {version} is available"
+ },
+ "userMessage.cancel": {
+ "defaultMessage": "Cancel"
+ },
+ "userMessage.cancelAriaLabel": {
+ "defaultMessage": "Cancel editing"
+ },
+ "userMessage.editAriaLabel": {
+ "defaultMessage": "Edit message content"
+ },
+ "userMessage.editButton": {
+ "defaultMessage": "Edit"
+ },
+ "userMessage.editInPlace": {
+ "defaultMessage": "Edit in Place"
+ },
+ "userMessage.editInPlaceAriaLabel": {
+ "defaultMessage": "Edit message in place"
+ },
+ "userMessage.editInPlaceDescription": {
+ "defaultMessage": "Edit in Place updates this session • Fork Session creates a new session"
+ },
+ "userMessage.editInPlaceTitle": {
+ "defaultMessage": "Update the message in this session"
+ },
+ "userMessage.editMessageAriaLabel": {
+ "defaultMessage": "Edit message: {preview}"
+ },
+ "userMessage.editMessageTitle": {
+ "defaultMessage": "Edit message"
+ },
+ "userMessage.editPlaceholder": {
+ "defaultMessage": "Edit your message..."
+ },
+ "userMessage.emptyError": {
+ "defaultMessage": "Message cannot be empty"
+ },
+ "userMessage.forkSession": {
+ "defaultMessage": "Fork Session"
+ },
+ "userMessage.forkSessionAriaLabel": {
+ "defaultMessage": "Fork session with edited message"
+ },
+ "userMessage.forkSessionTitle": {
+ "defaultMessage": "Create a new session with the edited message"
+ }
+}
diff --git a/ui/desktop/src/i18n/test-utils.tsx b/ui/desktop/src/i18n/test-utils.tsx
new file mode 100644
index 00000000..f1927b71
--- /dev/null
+++ b/ui/desktop/src/i18n/test-utils.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import { IntlProvider } from 'react-intl';
+
+/**
+ * Wraps a component tree with IntlProvider for tests.
+ * Uses English locale with no messages (defaultMessage values are used).
+ */
+export function IntlTestWrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts
index fad942ec..a1732e0d 100644
--- a/ui/desktop/src/main.ts
+++ b/ui/desktop/src/main.ts
@@ -535,6 +535,7 @@ let appConfig = {
GOOSE_API_HOST: 'https://localhost',
GOOSE_PATH_ROOT: resolveGoosePathRoot(),
GOOSE_WORKING_DIR: '',
+ GOOSE_LOCALE: process.env.GOOSE_LOCALE || undefined,
// If GOOSE_ALLOWLIST_WARNING env var is not set, defaults to false (strict blocking mode)
GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true',
};
diff --git a/ui/desktop/src/renderer.tsx b/ui/desktop/src/renderer.tsx
index 6d0eddc3..c9df7f1e 100644
--- a/ui/desktop/src/renderer.tsx
+++ b/ui/desktop/src/renderer.tsx
@@ -1,5 +1,6 @@
import React, { Suspense, lazy } from 'react';
import ReactDOM from 'react-dom/client';
+import { IntlProvider } from 'react-intl';
import { ConfigProvider } from './components/ConfigContext';
import { ErrorBoundary } from './components/ErrorBoundary';
import SuspenseLoader from './suspense-loader';
@@ -7,6 +8,7 @@ import { client } from './api/client.gen';
import { setTelemetryEnabled } from './utils/analytics';
import { readConfig } from './api';
import { applyThemeTokens } from './theme/theme-tokens';
+import { currentLocale, currentMessageLocale, loadMessages } from './i18n';
// Apply theme tokens to :root before first paint.
applyThemeTokens();
@@ -44,15 +46,19 @@ const TELEMETRY_CONFIG_KEY = 'GOOSE_TELEMETRY_ENABLED';
}
}
+ const messages = await loadMessages(currentMessageLocale);
+
ReactDOM.createRoot(document.getElementById('root')!).render(
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
})();
diff --git a/ui/desktop/src/utils/timeUtils.ts b/ui/desktop/src/utils/timeUtils.ts
index c7a9abb5..6de531d0 100644
--- a/ui/desktop/src/utils/timeUtils.ts
+++ b/ui/desktop/src/utils/timeUtils.ts
@@ -1,12 +1,13 @@
+import { currentLocale } from '../i18n';
+
export function formatMessageTimestamp(timestamp?: number): string {
const date = timestamp ? new Date(timestamp * 1000) : new Date();
const now = new Date();
- // Format time as HH:MM AM/PM
- const timeStr = date.toLocaleTimeString('en-US', {
+ // Format time using locale's default hour cycle
+ const timeStr = date.toLocaleTimeString(currentLocale, {
hour: 'numeric',
minute: '2-digit',
- hour12: true,
});
// Check if the message is from today
@@ -18,8 +19,8 @@ export function formatMessageTimestamp(timestamp?: number): string {
return timeStr;
}
- // If not today, format as MM/DD/YYYY HH:MM AM/PM
- const dateStr = date.toLocaleDateString('en-US', {
+ // If not today, format as localized date + time
+ const dateStr = date.toLocaleDateString(currentLocale, {
month: '2-digit',
day: '2-digit',
year: 'numeric',
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 4fb6835e..eef91dc1 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -151,6 +151,9 @@ importers:
react-icons:
specifier: ^5.5.0
version: 5.6.0(react@19.2.4)
+ react-intl:
+ specifier: ^10.1.0
+ version: 10.1.0(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3)
react-markdown:
specifier: ^10.1.0
version: 10.1.0(@types/react@19.2.14)(react@19.2.4)
@@ -239,6 +242,9 @@ importers:
'@eslint/js':
specifier: ^9.39.2
version: 9.39.4
+ '@formatjs/cli':
+ specifier: ^6.14.0
+ version: 6.14.0
'@hey-api/openapi-ts':
specifier: ^0.93.0
version: 0.93.1(magicast@0.5.2)(typescript@5.9.3)
@@ -402,7 +408,7 @@ importers:
version: 6.1.1
'@types/node':
specifier: ^25.2.3
- version: 25.5.0
+ version: 25.4.0
'@types/react':
specifier: ^19.2.0
version: 19.2.14
@@ -530,6 +536,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/runtime@7.28.6':
+ resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
+ engines: {node: '>=6.9.0'}
+
'@babel/runtime@7.29.2':
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
engines: {node: '>=6.9.0'}
@@ -1211,6 +1221,60 @@ packages:
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+ '@formatjs/bigdecimal@0.2.0':
+ resolution: {integrity: sha512-GeaxHZbUoYvHL9tC5eltHLs+1zU70aPw0s7LwqgktIzF5oMhNY4o4deEtusJMsq7WFJF3Ye2zQEzdG8beVk73w==}
+
+ '@formatjs/cli@6.14.0':
+ resolution: {integrity: sha512-5lqzAyAObZ8J8Ljtua8s4e5GKrPBu/lmuYN5ok5GgKUe+u+QwBPNlR3d2aYJpUbhgYuV8PXPHFXVPOwyhSR1uw==}
+ engines: {node: '>= 20.12.0'}
+ hasBin: true
+ peerDependencies:
+ '@glimmer/env': '*'
+ '@glimmer/reference': '*'
+ '@glimmer/syntax': ^0.84.3 || ^0.95.0
+ '@glimmer/validator': '*'
+ '@vue/compiler-core': ^3.5.0
+ content-tag: ^4.1.0
+ vue: ^3.5.0
+ peerDependenciesMeta:
+ '@glimmer/env':
+ optional: true
+ '@glimmer/reference':
+ optional: true
+ '@glimmer/syntax':
+ optional: true
+ '@glimmer/validator':
+ optional: true
+ '@vue/compiler-core':
+ optional: true
+ content-tag:
+ optional: true
+ vue:
+ optional: true
+
+ '@formatjs/ecma402-abstract@3.2.0':
+ resolution: {integrity: sha512-dHnqHgBo6GXYGRsepaE1wmsC2etaivOWd5VaJstZd+HI2zR3DCUjbDVZRtoPGkkXZmyHvBwrdEUuqfvzhF/DtQ==}
+
+ '@formatjs/fast-memoize@3.1.1':
+ resolution: {integrity: sha512-CbNbf+tlJn1baRnPkNePnBqTLxGliG6DDgNa/UtV66abwIjwsliPMOt0172tzxABYzSuxZBZfcp//qI8AvBWPg==}
+
+ '@formatjs/icu-messageformat-parser@3.5.3':
+ resolution: {integrity: sha512-HJWZ9S6JWey6iY5+YXE3Kd0ofWU1sC2KTTp56e1168g/xxWvVvr8k9G4fexIgwYV9wbtjY7kGYK5FjoWB3B2OQ==}
+
+ '@formatjs/icu-skeleton-parser@2.1.3':
+ resolution: {integrity: sha512-9mFp8TJ166ZM2pcjKwsBWXrDnOJGT7vMEScVgLygUODPOsE8S6f/FHoacvrlHK1B4dYZk8vSCNruyPU64AfgJQ==}
+
+ '@formatjs/intl-localematcher@0.8.2':
+ resolution: {integrity: sha512-q05KMYGJLyqFNFtIb8NhWLF5X3aK/k0wYt7dnRFuy6aLQL+vUwQ1cg5cO4qawEiINybeCPXAWlprY2mSBjSXAQ==}
+
+ '@formatjs/intl@4.1.4':
+ resolution: {integrity: sha512-y5aZ5p5zO74mZPdQMe9gq8SR1P6rlPQkaYOVFHyRzWrViif4sRKIQX01GYbKJvaGanupVVX+0WoKJqLVsP+X3Q==}
+ peerDependencies:
+ typescript: ^5.6.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
'@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
@@ -2878,6 +2942,9 @@ packages:
'@types/node@24.12.0':
resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==}
+ '@types/node@25.4.0':
+ resolution: {integrity: sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==}
+
'@types/node@25.5.0':
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
@@ -4631,6 +4698,9 @@ packages:
resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==}
engines: {node: '>=10.13.0'}
+ intl-messageformat@11.2.0:
+ resolution: {integrity: sha512-IhghAA8n4KSlXuWKzYsWyWb82JoYTzShfyvdSF85oJPnNOjvv4kAo7S7Jtkm3/vJ53C7dQNRO+Gpnj3iWgTjBQ==}
+
ip-address@10.1.0:
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
engines: {node: '>= 12'}
@@ -5954,6 +6024,16 @@ packages:
peerDependencies:
react: ^19.2.4
+ react-intl@10.1.0:
+ resolution: {integrity: sha512-SNF5gKE25E7Kw5vGGOQErXsLcDLYksbOkBCdVynHU+bLfCKCGBZef7tI13WarpSYm3D0Kx9tDBOOIVymaLHuSg==}
+ peerDependencies:
+ '@types/react': '19'
+ react: ^19.2.4
+ typescript: ^5.6.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -7317,6 +7397,8 @@ snapshots:
'@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6
+ '@babel/runtime@7.28.6': {}
+
'@babel/runtime@7.29.2': {}
'@babel/template@7.28.6':
@@ -8221,6 +8303,40 @@ snapshots:
'@floating-ui/utils@0.2.11': {}
+ '@formatjs/bigdecimal@0.2.0': {}
+
+ '@formatjs/cli@6.14.0': {}
+
+ '@formatjs/ecma402-abstract@3.2.0':
+ dependencies:
+ '@formatjs/bigdecimal': 0.2.0
+ '@formatjs/fast-memoize': 3.1.1
+ '@formatjs/intl-localematcher': 0.8.2
+
+ '@formatjs/fast-memoize@3.1.1': {}
+
+ '@formatjs/icu-messageformat-parser@3.5.3':
+ dependencies:
+ '@formatjs/ecma402-abstract': 3.2.0
+ '@formatjs/icu-skeleton-parser': 2.1.3
+
+ '@formatjs/icu-skeleton-parser@2.1.3':
+ dependencies:
+ '@formatjs/ecma402-abstract': 3.2.0
+
+ '@formatjs/intl-localematcher@0.8.2':
+ dependencies:
+ '@formatjs/fast-memoize': 3.1.1
+
+ '@formatjs/intl@4.1.4(typescript@5.9.3)':
+ dependencies:
+ '@formatjs/ecma402-abstract': 3.2.0
+ '@formatjs/fast-memoize': 3.1.1
+ '@formatjs/icu-messageformat-parser': 3.5.3
+ intl-messageformat: 11.2.0
+ optionalDependencies:
+ typescript: 5.9.3
+
'@gar/promisify@1.1.3': {}
'@hey-api/codegen-core@0.7.0(magicast@0.5.2)(typescript@5.9.3)':
@@ -9735,7 +9851,7 @@ snapshots:
'@testing-library/dom@10.4.1':
dependencies:
'@babel/code-frame': 7.29.0
- '@babel/runtime': 7.29.2
+ '@babel/runtime': 7.28.6
'@types/aria-query': 5.0.4
aria-query: 5.3.0
dom-accessibility-api: 0.5.16
@@ -9916,6 +10032,10 @@ snapshots:
dependencies:
undici-types: 7.16.0
+ '@types/node@25.4.0':
+ dependencies:
+ undici-types: 7.18.2
+
'@types/node@25.5.0':
dependencies:
undici-types: 7.18.2
@@ -12070,6 +12190,12 @@ snapshots:
interpret@3.1.1: {}
+ intl-messageformat@11.2.0:
+ dependencies:
+ '@formatjs/ecma402-abstract': 3.2.0
+ '@formatjs/fast-memoize': 3.1.1
+ '@formatjs/icu-messageformat-parser': 3.5.3
+
ip-address@10.1.0: {}
ipaddr.js@1.9.1: {}
@@ -13637,6 +13763,17 @@ snapshots:
dependencies:
react: 19.2.4
+ react-intl@10.1.0(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3):
+ dependencies:
+ '@formatjs/ecma402-abstract': 3.2.0
+ '@formatjs/icu-messageformat-parser': 3.5.3
+ '@formatjs/intl': 4.1.4(typescript@5.9.3)
+ '@types/react': 19.2.14
+ intl-messageformat: 11.2.0
+ react: 19.2.4
+ optionalDependencies:
+ typescript: 5.9.3
+
react-is@16.13.1: {}
react-is@17.0.2: {}