feat(desktop): add i18n infrastructure with react-intl (#8105)

This commit is contained in:
jh-block
2026-03-27 19:39:50 +01:00
committed by GitHub
parent d596113e72
commit a6765fbcd9
172 changed files with 13793 additions and 2054 deletions
@@ -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 (
<div className="space-y-4 pr-4 pb-8 mt-1">
<Card className="pb-2 rounded-lg">
<CardHeader className="pb-0">
<CardTitle className="">Mode</CardTitle>
<CardDescription>Configure how Goose interacts with tools and extensions</CardDescription>
<CardTitle className="">{intl.formatMessage(i18n.modeTitle)}</CardTitle>
<CardDescription>{intl.formatMessage(i18n.modeDescription)}</CardDescription>
</CardHeader>
<CardContent className="px-2">
<ModeSection sessionId={sessionId} />
@@ -34,8 +56,8 @@ export default function ChatSettingsSection({ sessionId }: { sessionId?: string
<Card className="pb-2 rounded-lg">
<CardHeader className="pb-0">
<CardTitle className="">Response Styles</CardTitle>
<CardDescription>Choose how Goose should format and style its responses</CardDescription>
<CardTitle className="">{intl.formatMessage(i18n.responseStylesTitle)}</CardTitle>
<CardDescription>{intl.formatMessage(i18n.responseStylesDescription)}</CardDescription>
</CardHeader>
<CardContent className="px-2">
<ResponseStylesSection />
@@ -10,51 +10,143 @@ import {
DialogTitle,
} from '../../ui/dialog';
import { errorMessage } from '../../../utils/conversionUtils';
import { defineMessages, useIntl } from '../../../i18n';
const HelpText = () => (
<div className="text-sm flex-col space-y-4 text-text-secondary">
<p>
.goosehints is a text file used to provide additional context about your project and improve
the communication with Goose.
</p>
<p>
Please make sure <span className="font-bold">Developer</span> 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.
</p>
<p>
See{' '}
<Button
variant="link"
className="text-blue-500 hover:text-blue-600 p-0 h-auto"
onClick={() =>
window.open('https://block.github.io/goose/docs/guides/using-goosehints/', '_blank')
}
>
using .goosehints
</Button>{' '}
for more information.
</p>
</div>
);
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 }) => (
<div className="text-sm text-text-secondary">
<div className="text-red-600">Error reading .goosehints file: {errorMessage(error)}</div>
</div>
);
const HelpText = () => {
const intl = useIntl();
const FileInfo = ({ filePath, found }: { filePath: string; found: boolean }) => (
<div className="text-sm font-medium mb-2">
{found ? (
<div className="text-green-600">
<Check className="w-4 h-4 inline-block" /> .goosehints file found at: {filePath}
return (
<div className="text-sm flex-col space-y-4 text-text-secondary">
<p>{intl.formatMessage(i18n.helpText1)}</p>
<p>
{intl.formatMessage(i18n.helpText2, {
bold: <span className="font-bold">{intl.formatMessage(i18n.developer)}</span>,
})}
</p>
<p>
{intl.formatMessage(i18n.helpText3, {
link: (
<Button
variant="link"
className="text-blue-500 hover:text-blue-600 p-0 h-auto"
onClick={() =>
window.open(
'https://block.github.io/goose/docs/guides/using-goosehints/',
'_blank'
)
}
>
{intl.formatMessage(i18n.helpTextLink)}
</Button>
),
})}
</p>
</div>
);
};
const ErrorDisplay = ({ error }: { error: Error }) => {
const intl = useIntl();
return (
<div className="text-sm text-text-secondary">
<div className="text-red-600">
{intl.formatMessage(i18n.errorReading, { error: errorMessage(error) })}
</div>
) : (
<div>Creating new .goosehints file at: {filePath}</div>
)}
</div>
);
</div>
);
};
const FileInfo = ({ filePath, found }: { filePath: string; found: boolean }) => {
const intl = useIntl();
return (
<div className="text-sm font-medium mb-2">
{found ? (
<div className="text-green-600">
<Check className="w-4 h-4 inline-block" />{' '}
{intl.formatMessage(i18n.fileFound, { filePath })}
</div>
) : (
<div>{intl.formatMessage(i18n.fileCreating, { filePath })}</div>
)}
</div>
);
};
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<string>('');
const [goosehintsFileFound, setGoosehintsFileFound] = useState<boolean>(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
<Dialog open={true} onOpenChange={(open) => setIsGoosehintsModalOpen(open)}>
<DialogContent className="w-[80vw] max-w-[80vw] sm:max-w-[80vw] max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle>Configure Project Hints (.goosehints)</DialogTitle>
<DialogDescription>
Provide additional context about your project to improve communication with Goose
</DialogDescription>
<DialogTitle>{intl.formatMessage(i18n.dialogTitle)}</DialogTitle>
<DialogDescription>{intl.formatMessage(i18n.dialogDescription)}</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto space-y-4 pt-2 pb-4">
@@ -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)}
/>
</div>
)}
@@ -136,14 +227,14 @@ export const GoosehintsModal = ({ directory, setIsGoosehintsModalOpen }: Goosehi
{saveSuccess && (
<span className="text-green-600 text-sm flex items-center gap-1 mr-auto">
<Check className="w-4 h-4" />
Saved successfully
{intl.formatMessage(i18n.savedSuccessfully)}
</span>
)}
<Button variant="outline" onClick={() => setIsGoosehintsModalOpen(false)}>
Close
{intl.formatMessage(i18n.close)}
</Button>
<Button onClick={writeFile} disabled={isSaving}>
{isSaving ? 'Saving...' : 'Save'}
{isSaving ? intl.formatMessage(i18n.saving) : intl.formatMessage(i18n.save)}
</Button>
</DialogFooter>
</DialogContent>
@@ -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 = () => {
<>
<div className="flex items-center justify-between px-2 py-2">
<div className="flex-1">
<h3 className="text-text-primary">Project Hints (.goosehints)</h3>
<h3 className="text-text-primary">{intl.formatMessage(i18n.title)}</h3>
<p className="text-xs text-text-secondary mt-[2px]">
Configure your project's .goosehints file to provide additional context to Goose
{intl.formatMessage(i18n.description)}
</p>
</div>
<Button
@@ -23,7 +41,7 @@ export const GoosehintsSection = () => {
className="flex items-center gap-2"
>
<FolderKey size={16} />
Configure
{intl.formatMessage(i18n.configure)}
</Button>
</div>
{isModalOpen && (
@@ -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 (
<div className="flex items-center justify-between py-2 px-2 hover:bg-background-secondary rounded-lg transition-all">
<div>
<h3 className="text-text-primary">Enable Spellcheck</h3>
<h3 className="text-text-primary">{intl.formatMessage(i18n.title)}</h3>
<p className="text-xs text-text-secondary max-w-md mt-[2px]">
Check spelling in the chat input. Requires restart to take effect.
{intl.formatMessage(i18n.description)}
</p>
</div>
<div className="flex items-center">