Add i18n module + translations

This commit is contained in:
Jordi Marzo 2023-05-13 15:44:57 +02:00
parent e60272eefa
commit a085dae2a8
17 changed files with 651 additions and 130 deletions

View file

@ -18,8 +18,8 @@
"@types/react-dom": "^18.0.11",
"buffer": "^5.7.1",
"parcel": "^2.8.3",
"path-browserify": "^1.0.1",
"parcel-reporter-static-files-copy": "^1.5.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10"
},
"dependencies": {
@ -32,8 +32,8 @@
"@tabler/icons-react": "^2.9.0",
"@tanstack/react-location": "^3.7.4",
"@types/node": "18.15.0",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"dexie": "^3.2.3",
"dexie-export-import": "^4.0.6",
"dexie-react-hooks": "^1.1.3",
@ -41,6 +41,8 @@
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"gpt-token-utils": "^1.2.0",
"i18next": "^22.4.15",
"i18next-browser-languagedetector": "^7.0.1",
"lodash": "^4.17.21",
"nanoid": "^4.0.1",
"next": "13.2.4",
@ -48,6 +50,7 @@
"openai-ext": "^1.2.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.2.2",
"react-icons": "^4.8.0",
"react-markdown": "^8.0.6",
"remark-gfm": "^3.0.1",

117
src/assets/i18n/en.json Normal file
View file

@ -0,0 +1,117 @@
{
"translation": {
"notAnotherUI": "Not just another ChatGPT user-interface!",
"changeOpenAIKey": "Change OpenAI Key",
"enterOpenAIKey": "Enter OpenAI Key",
"downloadDesktopApp": "Download Desktop App",
"freeAndOpenSource": {
"title": "Free and open source",
"description": "This app is provided for free and the source code is available on GitHub."
},
"privacyFocused": {
"title": "Privacy focused",
"description": "No tracking, no cookies, no bullshit. All your data is stored locally."
},
"bestExperience": {
"title": "Best experience",
"description": "Crafted with love and care to provide the best experience possible."
},
"search": "Search",
"lightMode": "Light Mode",
"darkMode": "Dark Mode",
"settings": "Settings",
"database": "Database",
"sourceCode": "Source Code",
"followOnTwitter": "Follow on Twitter",
"giveFeedback": "Give Feedback",
"chats": "Chats",
"newChat": "New Chat",
"prompts": "Prompts",
"newPrompt": "New Prompt",
"savePrompt": "Save Prompt",
"createPrompt": "Create Prompt",
"savedPrompt": {
"title": "Saved",
"message": "Prompt created"
},
"error": {
"title": "Error"
},
"networkError": {
"title": "Error",
"message": "No internet connection."
},
"chatIdError": {
"title": "Error",
"message": "chatId is not defined. Please create a chat to get started."
},
"apiKeyError": {
"title": "Error",
"message": "OpenAI API Key is not defined. Please set your API Key"
},
"title": "Title",
"content": "Content",
"save": "save",
"youAre": "You are",
"respondInTone": "Respond in",
"respondInStyle": "Respond in",
"tone": "tone",
"style": "style",
"youAreChatGPT": "You are ChatGPT, a large language model trained by OpenAI.",
"defineTitleChat": "What would be a short and relevant title for this chat ? You must strictly answer with only the title, no other text is allowed.",
"character": "Character",
"toneList": "Tone",
"styleList": "Style",
"format": "Format",
"yourMessageHere": "Your message here...",
"deleteChats": "Delete Chats",
"confirmDeleteChats": "Are you sure you want to delete your chats?",
"deleteChat": "Delete Chat",
"confirmDeleteChat": "Are you sure you want to delete this chat?",
"delete": "Delete",
"deleted": "Deleted",
"chatDeleted": "Chat deleted.",
"removeChatError": {
"title": "Error",
"message": "Can't remove chat. Please refresh the page and try again."
},
"openAIAPIKey": "OpenAI API Key",
"getYourOpenAIAPIkey": "Get your OpenAI API key",
"APIKeyStoredLocally": "The API Key is stored locally on your browser and never sent anywhere else.",
"openAIType": "OpenAI Type",
"openAIKeySaved": {
"title": "saved",
"message": "Your OpenAI Key has been saved."
},
"openAITypeSaved": {
"title": "saved",
"message": "Your OpenAI Type has been saved."
},
"openIATTypeCustom": "Custom (e.g. Azure OpenAI)",
"openAIModel": "OpenAI Model (OpenAI Only)",
"openAIModelSaved": {
"title": "saved",
"message": "Your OpenAI Model has been saved."
},
"warning": "Warning",
"warningCostMessage": "The displayed cost was not updated yet to reflect the costs for each model. Right now it will always show the cost for GPT-3.5 on OpenAI.",
"openAIAuth": "OpenAI Auth (Custom Only)",
"openAIAuthSaved": {
"title": "saved",
"message": "Your OpenAI Auth has been saved."
},
"none": "None",
"bearerToken": "Bearer Token",
"apiKey": "API Key",
"openAIBaseSaved": {
"title": "saved",
"message": "Your OpenAI Base has been saved."
},
"openAIAPIBase": "OpenAI API Base (Custom Only)",
"openAIAPIVersion": "OpenAI API Version (Custom Only)",
"openAIVersionSaved": {
"title": "saved",
"message": "Your OpenAI Version has been saved."
}
}
}

117
src/assets/i18n/fr.json Normal file
View file

@ -0,0 +1,117 @@
{
"translation": {
"notAnotherUI": "Ce n'est pas juste une autre interface de ChatGPT !",
"changeOpenAIKey": "Modifier la clé API d'OpenAI",
"enterOpenAIKey": "Entrer la clé API d'OpenAI",
"downloadDesktopApp": "Télécharger l'application de bureau",
"freeAndOpenSource": {
"title": "Gratuit et open source",
"description": "Cette application est gratuite et le code source est disponible sur GitHub."
},
"privacyFocused": {
"title": "Centré sur la vie privée",
"description": "Pas de suivi, pas de cookies, pas de mensonge. Toutes vos données sont stockées localement."
},
"bestExperience": {
"title": "La meilleure expérience",
"description": "Fabriqué avec amour et soin pour offrir la meilleure expérience possible."
},
"search": "Rechercher",
"lightMode": "Mode clair",
"darkMode": "Mode sombre",
"settings": "Paramètres",
"database": "Base de données",
"sourceCode": "Code source",
"followOnTwitter": "Suivez-nous sur Twitter",
"giveFeedback": "Donnez votre avis",
"chats": "Conversations",
"newChat": "Nouvelle conversation",
"prompts": "Prompts",
"newPrompt": "Nouveau Prompt",
"savePrompt": "Enregistrer le Prompt",
"createPrompt": "Créer un Prompt",
"savedPrompt": {
"title": "Sauvegardé",
"message": "Prompt créée"
},
"error": {
"title": "Erreur"
},
"networkError": {
"title": "Erreur",
"message": "Pas de connexion internet."
},
"chatIdError": {
"title": "Erreur",
"message": "le ChatId n'est pas défini. Veuillez créer une conversation pour commencer."
},
"apiKeyError": {
"title": "Erreur",
"message": "La clé API OpenAI n'est pas définie. Veuillez définir votre clé API"
},
"title": "Titre",
"content": "Contenu",
"save": "Enregistrer",
"youAre": "Tu es",
"respondInTone": "Réponds sur un ton",
"respondInStyle": "Réponds dans un style",
"tone": "",
"style": "",
"youAreChatGPT": "Tu es ChatGPT, un modèle de langage avancé entraîné par OpenAI.",
"defineTitleChat": "Titre court et pertinent pour cette discussion ? Vous devez répondre strictement avec le titre, aucun autre texte n'est autorisé.",
"character": "Personnage",
"toneList": "Ton",
"styleList": "Style",
"format": "Format",
"yourMessageHere": "Votre message ici...",
"deleteChats": "Supprimer les conversations",
"confirmDeleteChats": "Êtes-vous sûr de vouloir supprimer les conversations ?",
"deleteChat": "Supprimer la conversation",
"confirmDeleteChat": "Êtes-vous sûr de vouloir supprimer la conversation ?",
"delete": "Supprimer",
"deleted": "Supprimée",
"chatDeleted": "Conversation supprimée.",
"removeChatError": {
"title": "Erreur",
"message": "Impossible de supprimer la conversation. Veuillez actualiser la page et réessayer."
},
"openAIAPIKey": "Clé API OpenAI",
"getYourOpenAIAPIkey": "Obtenez votre clé API OpenAI",
"APIKeyStoredLocally": "La clé API est stockée localement sur votre navigateur et n'est jamais envoyée ailleurs.",
"openAIType": "Type d'OpenAI",
"openAIKeySaved": {
"title": "Sauvegardé",
"message": "Votre clé OpenAI a été enregistrée."
},
"openAITypeSaved": {
"title": "Sauvegardé",
"message": "Votre type d'OpenAI a été enregistré."
},
"openIATTypeCustom": "Personnalisé (par exemple: Azure OpenAI)",
"openAIModel": "Modèle OpenAI (OpenAI seulement)",
"openAIModelSaved": {
"title": "Sauvegardé",
"message": "Votre modèle OpenAI a été enregistré."
},
"warning": "Avertissement",
"warningCostMessage": "Le coût affiché n'a pas encore été mis à jour pour refléter les coûts de chaque modèle. Pour l'instant, le coût affiché est toujours celui du modèle GPT-3.5 de l'OpenAI.",
"openAIAuth": "Methode d'authentification OpenAI (personnalisé seulement)",
"openAIAuthSaved": {
"title": "Sauvegardé",
"message": "Votre methode d'authentification OpenAI a été enregistré."
},
"none": "Aucune",
"bearerToken": "Bearer Token",
"apiKey": "Clé API",
"openAIBaseSaved": {
"title": "Sauvegardé",
"message": "Votre URL d'API OpenAI a été enregistrée."
},
"openAIAPIBase": "URL d'API OpenAI (personnalisé uniquement)",
"openAIAPIVersion": "Version de l'API OpenAI (personnalisé uniquement)",
"openAIVersionSaved": {
"title": "Sauvegardé",
"message": "Votre version OpenAI a été enregistrée."
}
}
}

View file

@ -13,6 +13,8 @@ import { IconPlaylistAdd, IconPlus } from "@tabler/icons-react";
import { nanoid } from "nanoid";
import { useEffect, useState } from "react";
import { db } from "../db";
import '../i18n'
import {t} from "i18next";
export function CreatePromptModal({ content }: { content?: string }) {
const [opened, { open, close }] = useDisclosure(false);
@ -27,17 +29,17 @@ export function CreatePromptModal({ content }: { content?: string }) {
return (
<>
{content ? (
<Tooltip label="Save Prompt" position="left">
<Tooltip label={t('savePrompt')} position="left">
<ActionIcon onClick={open}>
<IconPlaylistAdd opacity={0.5} size={20} />
</ActionIcon>
</Tooltip>
) : (
<Button fullWidth onClick={open} leftIcon={<IconPlus size={20} />}>
New Prompt
{t('newPrompt')}
</Button>
)}
<Modal opened={opened} onClose={close} title="Create Prompt" size="lg">
<Modal opened={opened} onClose={close} title={t('createPrompt')} size="lg">
<form
onSubmit={async (event) => {
try {
@ -51,22 +53,22 @@ export function CreatePromptModal({ content }: { content?: string }) {
createdAt: new Date(),
});
notifications.show({
title: "Saved",
message: "Prompt created",
title: t('savedPrompt.title'),
message: t('savedPrompt.message'),
});
close();
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -78,14 +80,14 @@ export function CreatePromptModal({ content }: { content?: string }) {
>
<Stack>
<TextInput
label="Title"
label={t('title')}
value={title}
onChange={(event) => setTitle(event.currentTarget.value)}
formNoValidate
data-autofocus
/>
<Textarea
placeholder="Content"
placeholder={t('content')}
autosize
minRows={5}
maxRows={10}
@ -93,7 +95,7 @@ export function CreatePromptModal({ content }: { content?: string }) {
onChange={(event) => setValue(event.currentTarget.value)}
/>
<Button type="submit" loading={submitting}>
Save
{t('save')}
</Button>
</Stack>
</form>

View file

@ -6,6 +6,8 @@ import { cloneElement, ReactElement, useEffect, useState } from "react";
import { Chat, db } from "../db";
import { useApiKey } from "../hooks/useApiKey";
import { useChatId } from "../hooks/useChatId";
import '../i18n'
import {t} from "i18next";
export function DeleteChatModal({
chat,
@ -29,7 +31,7 @@ export function DeleteChatModal({
return (
<>
{cloneElement(children, { onClick: open })}
<Modal opened={opened} onClose={close} title="Delete Chat">
<Modal opened={opened} onClose={close} title={t('deleteChat')}>
<form
onSubmit={async (event) => {
try {
@ -43,22 +45,21 @@ export function DeleteChatModal({
close();
notifications.show({
title: "Deleted",
message: "Chat deleted.",
title: t('deleted'),
message: t('chatDeleted'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
} else {
notifications.show({
title: "Error",
title: t('removeChatError.title'),
color: "red",
message:
"Can't remove chat. Please refresh the page and try again.",
message: t('removeChatError.message'),
});
}
} finally {
@ -67,9 +68,9 @@ export function DeleteChatModal({
}}
>
<Stack>
<Text size="sm">Are you sure you want to delete this chat?</Text>
<Text size="sm">{t('confirmDeleteChat')}</Text>
<Button type="submit" color="red" loading={submitting}>
Delete
{t('delete')}
</Button>
</Stack>
</form>

View file

@ -2,6 +2,8 @@ import { Button, Modal, Stack, Text } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconTrash } from "@tabler/icons-react";
import { db } from "../db";
import '../i18n'
import {t} from "i18next";
export function DeleteChatsModal({ onOpen }: { onOpen: () => void }) {
const [opened, { open, close }] = useDisclosure(false, { onOpen });
@ -13,18 +15,18 @@ export function DeleteChatsModal({ onOpen }: { onOpen: () => void }) {
variant="outline"
color="red"
leftIcon={<IconTrash size={20} />}
>
Delete Chats
>"
{t("deleteChats")}
</Button>
<Modal
opened={opened}
onClose={close}
title="Delete Chats"
title={t("deleteChats")}
size="md"
withinPortal
>
<Stack>
<Text size="sm">Are you sure you want to delete your chats?</Text>
<Text size="sm">{t("confirmDeleteChats")}</Text>
<Button
onClick={async () => {
await db.chats.clear();
@ -34,7 +36,7 @@ export function DeleteChatsModal({ onOpen }: { onOpen: () => void }) {
}}
color="red"
>
Delete
{t("delete")}
</Button>
</Stack>
</Modal>

View file

@ -41,6 +41,8 @@ import { LogoText } from "./Logo";
import { Prompts } from "./Prompts";
import { SettingsModal } from "./SettingsModal";
import { config } from "../utils/config";
import '../i18n'
import {t} from "i18next";
declare global {
interface Window {
@ -51,7 +53,8 @@ declare global {
export function Layout() {
const theme = useMantineTheme();
const [opened, setOpened] = useState(false);
const [tab, setTab] = useState<"Chats" | "Prompts">("Chats");
// @ts-ignore
const [tab, setTab] = useState<"Chats" | "Prompts">(t('chats'));
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const navigate = useNavigate();
const router = useRouter();
@ -126,10 +129,10 @@ export function Layout() {
fullWidth
value={tab}
onChange={(value) => setTab(value as typeof tab)}
data={["Chats", "Prompts"]}
data={[t('chats'), t('prompts')]}
/>
<Box sx={{ padding: 4 }}>
{tab === "Chats" && (
{tab === t('chats') && (
<Button
fullWidth
leftIcon={<IconPlus size={20} />}
@ -137,17 +140,17 @@ export function Layout() {
const id = nanoid();
db.chats.add({
id,
description: "New Chat",
description: t('newChat'),
totalTokens: 0,
createdAt: new Date(),
});
navigate({ to: `/chats/${id}` });
}}
>
New Chat
{t('newChat')}
</Button>
)}
{tab === "Prompts" && <CreatePromptModal />}
{tab === t('prompts') && <CreatePromptModal />}
</Box>
</Navbar.Section>
<Navbar.Section
@ -163,7 +166,7 @@ export function Layout() {
<TextInput
variant="unstyled"
radius={0}
placeholder="Search"
placeholder={t('search')}
value={search}
onChange={(event) =>
setSearch(event.currentTarget.value.toLowerCase())
@ -180,8 +183,8 @@ export function Layout() {
/>
</Navbar.Section>
<Navbar.Section grow component={ScrollArea}>
{tab === "Chats" && <Chats search={search} />}
{tab === "Prompts" && (
{tab === t('chats') && <Chats search={search} />}
{tab === t('prompts') && (
<Prompts search={search} onPlay={() => setTab("Chats")} />
)}
</Navbar.Section>
@ -189,7 +192,7 @@ export function Layout() {
<Center>
{config.allowDarkModeToggle && (
<Tooltip
label={colorScheme === "dark" ? "Light Mode" : "Dark Mode"}
label={colorScheme === "dark" ? t('lightMode') : t('darkMode')}
>
<ActionIcon
sx={{ flex: 1 }}
@ -206,7 +209,7 @@ export function Layout() {
)}
{config.allowSettingsModal && (
<SettingsModal>
<Tooltip label="Settings">
<Tooltip label={t('settings')}>
<ActionIcon sx={{ flex: 1 }} size="xl">
<IconSettings size={20} />
</ActionIcon>
@ -215,14 +218,14 @@ export function Layout() {
)}
{config.allowDatabaseModal && (
<DatabaseModal>
<Tooltip label="Database">
<Tooltip label={t('database')}>
<ActionIcon sx={{ flex: 1 }} size="xl">
<IconDatabase size={20} />
</ActionIcon>
</Tooltip>
</DatabaseModal>
)}
<Tooltip label="Source Code">
<Tooltip label={t('sourceCode')}>
<ActionIcon
component="a"
href="https://github.com/deiucanta/chatpad"
@ -234,7 +237,7 @@ export function Layout() {
</ActionIcon>
</Tooltip>
{config.showTwitterLink && (
<Tooltip label="Follow on Twitter">
<Tooltip label={t('followOnTwitter')}>
<ActionIcon
component="a"
href="https://twitter.com/deiucanta"
@ -247,7 +250,7 @@ export function Layout() {
</Tooltip>
)}
{config.showFeedbackLink && (
<Tooltip label="Give Feedback">
<Tooltip label={t('giveFeedback')}>
<ActionIcon
component="a"
href="https://feedback.chatpad.ai"

View file

@ -18,6 +18,8 @@ import { cloneElement, ReactElement, useEffect, useState } from "react";
import { db } from "../db";
import { config } from "../utils/config";
import { checkOpenAIKey } from "../utils/openai";
import '../i18n'
import {t} from "i18next";
export function SettingsModal({ children }: { children: ReactElement }) {
const [opened, { open, close }] = useDisclosure(false);
@ -58,7 +60,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
return (
<>
{cloneElement(children, { onClick: open })}
<Modal opened={opened} onClose={close} title="Settings" size="lg">
<Modal opened={opened} onClose={close} title={t('settings')} size="lg">
<Stack>
<form
onSubmit={async (event) => {
@ -71,21 +73,21 @@ export function SettingsModal({ children }: { children: ReactElement }) {
console.log(apiKey);
});
notifications.show({
title: "Saved",
message: "Your OpenAI Key has been saved.",
title: t('openAIKeySaved.title'),
message: t('openAIKeySaved.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -97,7 +99,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
>
<Flex gap="xs" align="end">
<PasswordInput
label="OpenAI API Key"
label={t('openAIAPIKey')}
placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
sx={{ flex: 1 }}
value={value}
@ -105,7 +107,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
formNoValidate
/>
<Button type="submit" loading={submitting}>
Save
{t('save')}
</Button>
</Flex>
</form>
@ -116,19 +118,18 @@ export function SettingsModal({ children }: { children: ReactElement }) {
href="https://platform.openai.com/account/api-keys"
target="_blank"
>
Get your OpenAI API key
{t('getYourOpenAIAPIkey')}
</Anchor>
</Text>
</List.Item>
<List.Item>
<Text size="sm" color="dimmed">
The API Key is stored locally on your browser and never sent
anywhere else.
{t('APIKeyStoredLocally')}
</Text>
</List.Item>
</List>
<Select
label="OpenAI Type"
label={t('openAIType')}
value={type}
onChange={async (value) => {
setSubmitting(true);
@ -137,21 +138,21 @@ export function SettingsModal({ children }: { children: ReactElement }) {
openAiApiType: value ?? 'openai',
});
notifications.show({
title: "Saved",
message: "Your OpenAI Type has been saved.",
title: t('openAIKeySaved.title'),
message: t('openAIKeySaved.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -161,10 +162,10 @@ export function SettingsModal({ children }: { children: ReactElement }) {
}
}}
withinPortal
data={[{ "value": "openai", "label": "OpenAI"}, { "value": "custom", "label": "Custom (e.g. Azure OpenAI)"}]}
data={[{ "value": "openai", "label": "OpenAI"}, { "value": "custom", "label": t('openIATTypeCustom')}]}
/>
<Select
label="OpenAI Model (OpenAI Only)"
label={t('openAIModel')}
value={model}
onChange={async (value) => {
setSubmitting(true);
@ -173,21 +174,21 @@ export function SettingsModal({ children }: { children: ReactElement }) {
openAiModel: value ?? undefined,
});
notifications.show({
title: "Saved",
message: "Your OpenAI Model has been saved.",
title: t('openAIModelSaved.title'),
message: t('openAIModelSaved.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -199,12 +200,11 @@ export function SettingsModal({ children }: { children: ReactElement }) {
withinPortal
data={config.availableModels}
/>
<Alert color="orange" title="Warning">
The displayed cost was not updated yet to reflect the costs for each
model. Right now it will always show the cost for GPT-3.5 on OpenAI.
<Alert color="orange" title={t('warning')}>
{t('warningCostMessage')}
</Alert>
<Select
label="OpenAI Auth (Custom Only)"
label={t('openAIAuth')}
value={auth}
onChange={async (value) => {
setSubmitting(true);
@ -213,21 +213,21 @@ export function SettingsModal({ children }: { children: ReactElement }) {
openAiApiAuth: value ?? 'none',
});
notifications.show({
title: "Saved",
message: "Your OpenAI Auth has been saved.",
title: t('openAIModelSaved.title'),
message: t('openAIModelSaved.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -237,7 +237,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
}
}}
withinPortal
data={[{ "value": "none", "label": "None"}, { "value": "bearer-token", "label": "Bearer Token"}, { "value": "api-key", "label": "API Key"}]}
data={[{ "value": "none", "label": t('none')}, { "value": "bearer-token", "label": t('bearerToken')}, { "value": "api-key", "label": t('apiKey')}]}
/>
<form
onSubmit={async (event) => {
@ -249,21 +249,21 @@ export function SettingsModal({ children }: { children: ReactElement }) {
console.log(row);
});
notifications.show({
title: "Saved",
message: "Your OpenAI Base has been saved.",
title: t('openAIBaseSaved.title'),
message: t('openAIBaseSaved.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -275,7 +275,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
>
<Flex gap="xs" align="end">
<TextInput
label="OpenAI API Base (Custom Only)"
label={t('openAIAPIBase')}
placeholder="https://<resource-name>.openai.azure.com/openai/deployments/<deployment>"
sx={{ flex: 1 }}
value={base}
@ -283,7 +283,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
formNoValidate
/>
<Button type="submit" loading={submitting}>
Save
{t('save')}
</Button>
</Flex>
</form>
@ -297,21 +297,21 @@ export function SettingsModal({ children }: { children: ReactElement }) {
console.log(row);
});
notifications.show({
title: "Saved",
message: "Your OpenAI Version has been saved.",
title: t('openAIVersionSaved.title'),
message: t('openAIVersionSaved.message'),
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -323,7 +323,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
>
<Flex gap="xs" align="end">
<TextInput
label="OpenAI API Version (Custom Only)"
label={t('openAIAPIVersion')}
placeholder="2023-03-15-preview"
sx={{ flex: 1 }}
value={version}
@ -331,7 +331,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
formNoValidate
/>
<Button type="submit" loading={submitting}>
Save
{t('save')}
</Button>
</Flex>
</form>

22
src/i18n.tsx Normal file
View file

@ -0,0 +1,22 @@
import i18n from "i18next";
import {initReactI18next} from "react-i18next";
import LanguageDetector from 'i18next-browser-languagedetector';
import * as en from './assets/i18n/en.json';
import * as fr from './assets/i18n/fr.json';
export const resources = {
en,
fr
};
i18n
.use(initReactI18next)
.use(LanguageDetector)
.init({
resources,
fallbackLng: "en",
interpolation: {
escapeValue: false
}
});
export default i18n;

11
src/react-i18next.d.tsx Normal file
View file

@ -0,0 +1,11 @@
// import the original type declarations
import 'react-i18next';
// import all namespaces (for the default language, only)
import { resources, defaultNS } from './i18n';
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: typeof defaultNS;
resources: typeof resources['en'];
}
}

View file

@ -24,6 +24,8 @@ import {
createChatCompletion,
createStreamChatCompletion,
} from "../utils/openai";
import '../i18n'
import {t} from "i18next";
export function ChatRoute() {
const chatId = useChatId();
@ -55,14 +57,13 @@ export function ChatRoute() {
const getSystemMessage = () => {
const message: string[] = [];
if (writingCharacter) message.push(`You are ${writingCharacter}.`);
if (writingTone) message.push(`Respond in ${writingTone} tone.`);
if (writingStyle) message.push(`Respond in ${writingStyle} style.`);
// @ts-ignore
if (writingCharacter) message.push(t('youAre') + ` ${writingCharacter}.`);
if (writingTone) message.push(t('respondInTone') + ` ${writingTone} ` + t('tone'));
if (writingStyle) message.push(t('respondInStyle') +` ${writingStyle} ` + t('style'));
if (writingFormat) message.push(writingFormat);
if (message.length === 0)
message.push(
"You are ChatGPT, a large language model trained by OpenAI."
);
message.push(t('youAreChatGPT'));
return message.join(" ");
};
@ -71,18 +72,18 @@ export function ChatRoute() {
if (!chatId) {
notifications.show({
title: "Error",
title: t('chatIdError.title'),
color: "red",
message: "chatId is not defined. Please create a chat to get started.",
message: t('chatIdError.message'),
});
return;
}
if (!apiKey) {
notifications.show({
title: "Error",
title: t('apiKeyError.title'),
color: "red",
message: "OpenAI API Key is not defined. Please set your API Key",
message: t('apiKeyError.message'),
});
return;
}
@ -127,7 +128,7 @@ export function ChatRoute() {
setSubmitting(false);
if (chat?.description === "New Chat") {
if (chat?.description === t('newChat')) {
const messages = await db.messages
.where({ chatId })
.sortBy("createdAt");
@ -142,8 +143,7 @@ export function ChatRoute() {
})),
{
role: "user",
content:
"What would be a short and relevant title for this chat ? You must strictly answer with only the title, no other text is allowed.",
content: t('defineTitleChat'),
},
]);
const chatDescription =
@ -151,7 +151,7 @@ export function ChatRoute() {
if (createChatDescription.data.usage) {
await db.chats.where({ id: chatId }).modify((chat) => {
chat.description = chatDescription ?? "New Chat";
chat.description = chatDescription ?? t('newChat');
if (chat.totalTokens) {
chat.totalTokens +=
createChatDescription.data.usage!.total_tokens;
@ -164,15 +164,15 @@ export function ChatRoute() {
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
title: t('networkError.title'),
color: "red",
message: "No internet connection.",
message: t('networkError.message'),
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
title: t('error.title'),
color: "red",
message,
});
@ -264,7 +264,7 @@ export function ChatRoute() {
value={writingCharacter}
onChange={setWritingCharacter}
data={config.writingCharacters}
placeholder="Character"
placeholder={t('character')}
variant="filled"
searchable
clearable
@ -274,7 +274,7 @@ export function ChatRoute() {
value={writingTone}
onChange={setWritingTone}
data={config.writingTones}
placeholder="Tone"
placeholder={t('toneList')}
variant="filled"
searchable
clearable
@ -284,7 +284,7 @@ export function ChatRoute() {
value={writingStyle}
onChange={setWritingStyle}
data={config.writingStyles}
placeholder="Style"
placeholder={t('styleList')}
variant="filled"
searchable
clearable
@ -294,7 +294,7 @@ export function ChatRoute() {
value={writingFormat}
onChange={setWritingFormat}
data={config.writingFormats}
placeholder="Format"
placeholder={t('format')}
variant="filled"
searchable
clearable
@ -306,7 +306,7 @@ export function ChatRoute() {
<Textarea
key={chatId}
sx={{ flex: 1 }}
placeholder="Your message here..."
placeholder={t('yourMessageHere')}
autosize
autoFocus
disabled={submitting}

View file

@ -20,6 +20,8 @@ import { Logo } from "../components/Logo";
import { SettingsModal } from "../components/SettingsModal";
import { db } from "../db";
import { config } from "../utils/config";
import '../i18n'
import {t} from "i18next";
export function IndexRoute() {
const settings = useLiveQuery(() => db.settings.get("general"));
@ -34,7 +36,7 @@ export function IndexRoute() {
<Logo style={{ maxWidth: 240 }} />
</Text>
<Text mt={4} size="xl">
Not just another ChatGPT user-interface!
{t('notAnotherUI')}
</Text>
<SimpleGrid
mt={50}
@ -64,7 +66,7 @@ export function IndexRoute() {
variant={openAiApiKey ? "light" : "filled"}
leftIcon={<IconKey size={20} />}
>
{openAiApiKey ? "Change OpenAI Key" : "Enter OpenAI Key"}
{openAiApiKey ? t('changeOpenAIKey') : t('enterOpenAIKey')}
</Button>
</SettingsModal>
)}
@ -77,7 +79,7 @@ export function IndexRoute() {
variant="outline"
leftIcon={<IconCloudDownload size={20} />}
>
Download Desktop App
{t('downloadDesktopApp')}
</Button>
)}
</Group>
@ -90,20 +92,17 @@ export function IndexRoute() {
const features = [
{
icon: IconCurrencyDollar,
title: "Free and open source",
description:
"This app is provided for free and the source code is available on GitHub.",
title: t('freeAndOpenSource.title'),
description: t('freeAndOpenSource.description'),
},
{
icon: IconLock,
title: "Privacy focused",
description:
"No tracking, no cookies, no bullshit. All your data is stored locally.",
title: t('privacyFocused.title'),
description: t('privacyFocused.description'),
},
{
icon: IconNorthStar,
title: "Best experience",
description:
"Crafted with love and care to provide the best experience possible.",
title: t('bestExperience.title'),
description: t('bestExperience.description'),
},
];

View file

@ -1,5 +1,5 @@
{
"defaultModel": "gpt-3.5-turbo",
"defaultModel": "gpt-3.5-turbo",
"defaultType": "openai",
"defaultAuth": "api-key",
"defaultBase": "",

207
src/static/config_fr.json Normal file
View file

@ -0,0 +1,207 @@
{
"defaultModel": "gpt-3.5-turbo",
"defaultType": "openai",
"defaultAuth": "api-key",
"defaultBase": "",
"defaultVersion": "",
"defaultKey": "",
"availableModels": [
{
"value": "gpt-3.5-turbo",
"label": "GPT-3.5-TURBO (ChatGPT par défaut)"
},
{
"value": "gpt-3.5-turbo-0301",
"label": "GPT-3.5-TURBO-0301"
},
{
"value": "gpt-4",
"label": "GPT-4 (Bêta limitée)"
},
{
"value": "gpt-4-0314",
"label": "GPT-4-0314 (Bêta limitée)"
},
{
"value": "gpt-4-32k",
"label": "GPT-4-32K (Bêta limitée)"
},
{
"value": "gpt-4-32k-0314",
"label": "GPT-4-32K-0314 (Bêta limitée)"
}
],
"writingCharacters": [
{
"label": "Humoriste",
"value": "Un artiste de spectacle qui divertit son public en racontant des blagues et des histoires humoristiques."
},
{
"label": "Coach de vie",
"value": "Un professionnel qui aide les individus à identifier et atteindre leurs objectifs personnels et professionnels."
},
{
"label": "Conseiller en orientation",
"value": "Un professionnel qui aide les individus à explorer et à choisir des carrières, à développer des stratégies de recherche d'emploi et à améliorer leur performance professionnelle."
},
{
"label": "Nutritionniste",
"value": "Un professionnel de la santé qui se spécialise dans l'étude de la nutrition et de ses effets sur le corps."
},
{
"label": "Chef de produit",
"value": "Un professionnel qui supervise le développement et la commercialisation des produits d'une entreprise."
},
{
"label": "Entraîneur personnel",
"value": "Un professionnel de la remise en forme qui travaille avec des individus pour élaborer des programmes d'exercices personnalisés et améliorer leur santé et leur condition physique globales."
},
{
"label": "Expert en astuces de vie",
"value": "Une personne qui utilise des méthodes non conventionnelles pour résoudre des problèmes et augmenter la productivité dans la vie quotidienne."
},
{
"label": "Conseiller en voyages",
"value": "Un professionnel qui aide les individus à planifier et réserver des arrangements de voyage."
},
{
"label": "Coach pleine conscience",
"value": "Un professionnel qui aide les individus à développer des pratiques de pleine conscience pour réduire le stress et améliorer le bien-être."
},
{
"label": "Conseiller financier",
"value": "Un professionnel qui fournit des conseils et des orientations sur la planification financière, les stratégies d'investissement et la planification de la retraite."
},
{
"label": "Tuteur linguistiques",
"value": "Un enseignant qui aide les individus à apprendre et à améliorer leurs compétences linguistiques."
},
{
"label": "Guide touristique",
"value": "Un professionnel qui guide les visites et fournit des informations sur les attractions locales et la culture."
},
{
"label": "Expert marketing",
"value": "Un professionnel qui développe et met en oeuvre des stratégies de marketing pour promouvoir des produits et services."
},
{
"label": "Développeur logiciels",
"value": "Un professionnel qui conçoit, développe et maintient des applications et des systèmes informatiques."
},
{
"label": "Coach relations amoureuses",
"value": "Un professionnel qui aide les individus à améliorer leurs compétences en matière de rencontres et de relations amoureuses."
},
{
"label": "Expert en bricolage",
"value": "Une personne qui est habile à réaliser une large gamme de projets de bricolage autour de la maison."
},
{
"label": "Journaliste",
"value": "Un professionnel qui enquête et rapporte sur les événements et les nouvelles en cours."
},
{
"label": "Écrivain technique",
"value": "Un professionnel qui écrit sur la technologie et les sujets connexes pour une variété d'audiences."
},
{
"label": "Chef cuisinier",
"value": "Un professionnel de la cuisine qui prépare des repas et gère les opérations de la cuisine."
},
{
"label": "Vendeur professionnel",
"value": "Un professionnel qui vend des produits et services aux entreprises et aux consommateurs."
},
{
"label": "Avocat startup technologique",
"value": "Un professionnel du droit qui se spécialise dans la fourniture de conseils et de services juridiques aux entreprises technologiques en démarrage."
},
{
"label": "Designer graphique",
"value": "Un professionnel qui conçoit des supports visuels tels que des logos, des brochures et des sites web."
},
{
"label": "Chercheur universitaire",
"value": "Un professionnel qui mène des recherches et produit des travaux universitaires dans un domaine universitaire particulier."
},
{
"label": "Soutien à la clientèle",
"value": "Un professionnel qui fournit une assistance et un soutien aux clients ayant des questions ou des problèmes avec les produits ou services d'une entreprise."
},
{
"label": "Consultant RH",
"value": "Un professionnel qui fournit des conseils et des orientations aux organisations en matière de gestion et de stratégie des ressources humaines."
}
],
"writingTones": [
"Affirmé",
"Autoritaire",
"Détendu",
"Confiant",
"Condescendant",
"Conversationnel",
"Diplomatique",
"Direct",
"Éloquent",
"Formel",
"Amical",
"Humoristique",
"Informatif",
"Inspirant",
"Intense",
"Irritable",
"Blagueur",
"Polie",
"Sarcastique",
"Sincère",
"Apaisant",
"Sévère",
"Compassionnel",
"Tactique",
"Spirituel"
],
"writingStyles": [
"Académique",
"Analytique",
"Argumentatif",
"Conversationnel",
"Créatif",
"Critique",
"Descriptif",
"Explicatif",
"Informatif",
"Instructif",
"Investigatif",
"Journalistique",
"Métaphorique",
"Narratif",
"Persuasif",
"Poétique",
"Satirique",
"Technique"
],
"writingFormats": [
{
"value": "Réponds de manière concise autant que possible",
"label": "Concis"
},
{
"value": "Réfléchir étape par étape",
"label": "Étape par étape"
},
{
"value": "Réponds avec un niveau de détail minutieux",
"label": "Détailler"
},
{
"value": "Explique comme si j'avais cinq ans",
"label": "Explique comme si j'avais 5 ans"
}
],
"showDownloadLink": true,
"allowDarkModeToggle": true,
"allowSettingsModal": true,
"allowDatabaseModal": true,
"showTwitterLink": true,
"showFeedbackLink": true
}

View file

@ -1,3 +1,5 @@
import i18n from "i18next";
interface Config {
defaultModel: AvailableModel["value"];
defaultType: 'openai' | 'custom';
@ -36,7 +38,7 @@ interface WritingFormat {
export let config: Config;
export const loadConfig = async () => {
const response = await fetch("config.json");
const response = await fetch("config_"+i18n.resolvedLanguage+".json");
config = await response.json();
return config;
};

View file

@ -5,6 +5,7 @@
"esModuleInterop": true,
"lib": ["es2018", "dom"],
"strict": true,
"skipLibCheck": true
"skipLibCheck": true,
"resolveJsonModule": true
}
}

View file

@ -35,7 +35,7 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7":
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.3", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7":
version "7.21.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200"
integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==
@ -2741,6 +2741,13 @@ html-dom-parser@1.2.0:
domhandler "4.3.1"
htmlparser2 "7.2.0"
html-parse-stringify@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
dependencies:
void-elements "3.1.0"
html-react-parser@1.4.12:
version "1.4.12"
resolved "https://registry.yarnpkg.com/html-react-parser/-/html-react-parser-1.4.12.tgz#5d4336e3853a75e4ac678c9815c15c58581bb30e"
@ -2791,6 +2798,20 @@ human-signals@^4.3.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2"
integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==
i18next-browser-languagedetector@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.1.tgz#ead34592edc96c6c3a618a51cb57ad027c5b5d87"
integrity sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==
dependencies:
"@babel/runtime" "^7.19.4"
i18next@^22.4.15:
version "22.4.15"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-22.4.15.tgz#951882b751872994f8502b5a6ef6f796e6a7d7f8"
integrity sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==
dependencies:
"@babel/runtime" "^7.20.6"
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@ -4232,6 +4253,14 @@ react-error-overlay@6.0.9:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
react-i18next@^12.2.2:
version "12.2.2"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-12.2.2.tgz#38a6fad11acf4f2abfc5611bdb6b1918d0f47578"
integrity sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg==
dependencies:
"@babel/runtime" "^7.20.6"
html-parse-stringify "^3.0.1"
react-icons@^4.8.0:
version "4.8.0"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.8.0.tgz#621e900caa23b912f737e41be57f27f6b2bff445"
@ -5036,6 +5065,11 @@ vfile@^5.0.0:
unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0"
void-elements@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
weak-lru-cache@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19"