Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added an initial shape of automatic translations #58

Merged
merged 3 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions app/api/chatGpt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { NextRequest } from "next/server"
import { I18n, I18nInfo, I18nLang, ProjectSettings } from "@/store/useI18nState"
import { OpenAIHelper } from "@/utils/OpenAiUtils"
import { getServerSession } from "next-auth/next"
import * as z from "zod"

import { authOptions } from "@/lib/auth"
import { db } from "@/lib/db"
import { handleCatchApi } from "@/lib/exceptions"
import i18n from "@/lib/i18n"
import { ErrorResponse } from "@/lib/response"

const generateTranslationSchema = z.object({
projectId: z.string(),
keyword: z.string(),
})

export async function GET(req: NextRequest) {
try {
const session = await getServerSession(authOptions)

if (!session) {
return new Response("Unauthorized", { status: 403 })
}

const searchParams = req.nextUrl.searchParams

const { keyword, projectId } = generateTranslationSchema.parse({
projectId: searchParams.get("projectId"),
keyword: searchParams.get("keyword"),
})

const project = await db.project.findFirst({
where: {
id: projectId,
},
})

if (!project) {
return ErrorResponse({
error: i18n.t("The project does not exist or is not published"),
})
}

const languages = project.languages as I18nLang[]

if (!languages) {
return ErrorResponse({
error: i18n.t("The project does not have any languages"),
})
}

const languagesPropt = languages
.map((language) => language.short)
.join(", ")

const { context } = (project.info as I18nInfo[])?.find(
(ele) => ele.key === keyword
) || {
key: keyword,
context: "",
}

const settings = project.settings as ProjectSettings

try {
const prompt = `
I'm working on internationalizing my application. I'd like to translate the text "${keyword}" ${
context ? `, used in this context: "${context}"` : ""
}. Could you write the translations in [${languagesPropt}]?
[[Translations should be informal]] <-- ${settings.formality}
Matergi marked this conversation as resolved.
Show resolved Hide resolved
[[in the tone of a tech website]] <-- ${settings.description}
[[The target audience is both male and female]] <-- ${settings.audience}
with an age range between ${settings.ageStart} and ${settings.ageEnd} years old.

respond using an unique JSON object without any comments or any other descriptions, like so:
{
"en": "",
"it": "",
"es": ""
}

where:
language-id for english = en
language-id for italian = it
language-id for spanish = es

`

const openaiHelper = new OpenAIHelper()
const response = await openaiHelper.askChatGPT({
prompt,
})
const jsonString = openaiHelper.getResponseJSONString(response)
const result = openaiHelper.parseChatGPTJSONString<I18n>(jsonString)
if (result) {
return new Response(JSON.stringify(result))
}
throw new Error("Failed to get response from Chat GPT.")
} catch (e) {
throw new Error(e)
}
} catch (error) {
return handleCatchApi(error)
}
}
8 changes: 3 additions & 5 deletions components/app/project/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import * as React from "react"
import Link from "next/link"
import { Project } from "@prisma/client"

import "@/styles/editor.css"
import { useState } from "react"
Expand All @@ -16,13 +15,11 @@ import { DownloadKeywordsDropdownMenu } from "./dialogs/download"
import ImportKeywordsModal from "./dialogs/import-keywords"
import ProjectSettingsSlideOver from "./settings-slide-over"
import Table from "./table/table"
import { ProjectData } from "./types"
import useTranslation from "./useTranslation"

export interface EditorProps {
project: Pick<
Project,
"id" | "title" | "languages" | "published" | "info" | "settings"
>
project: ProjectData
}

export function Editor(props: EditorProps) {
Expand Down Expand Up @@ -117,6 +114,7 @@ export function Editor(props: EditorProps) {
editKey={editKey}
checkIfKeyAlreadyExists={checkIfKeyAlreadyExists}
isSaving={isSaving}
project={project}
/>
</div>
{isProjectSettingsOpened && (
Expand Down
43 changes: 31 additions & 12 deletions components/app/project/table/detail-slide-over.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ChangeEvent, useCallback, useState } from "react"

import i18n from "@/lib/i18n"
import { Button } from "@/components/ui/button"
import SlideOver, { SlideOverRow } from "@/components/slide-over"

import { ProjectData } from "../types"
import { Keyword } from "../useTranslation"

type Props = {
Expand All @@ -13,6 +15,7 @@ type Props = {
editContext: (key: string, context: string) => void
editKey: (key: string, newKey: string) => void
checkIfKeyAlreadyExists: (key: string) => boolean
project: ProjectData
}

const DetailSlideOver = (props: Props) => {
Expand All @@ -24,6 +27,7 @@ const DetailSlideOver = (props: Props) => {
editContext,
editKey,
checkIfKeyAlreadyExists,
project,
} = props

const [key, setKey] = useState(keyword.key)
Expand Down Expand Up @@ -59,37 +63,52 @@ const DetailSlideOver = (props: Props) => {
editKey(keyword.key, key)
}, [editKey, key, keyword.key])

const askAI = useCallback(async () => {
davidecarpini marked this conversation as resolved.
Show resolved Hide resolved
const response = await fetch(
`/api/chatGpt?projectId=${project?.id}&keyword=${keyword.key}`
).then((res) => res.json())
Object.keys(response).forEach((language) => {
editTranslation(language, keyword.key, response[language])
})
}, [editTranslation, keyword.key, project?.id])

return (
<SlideOver title={keyword.key} onClose={onClose} isSaving={isSaving}>
<div className="relative p-4 flex-1 sm:px-6">
<SlideOverRow title="Keyword">
<div className="mt-1">
<label className="inline-block text-xs font-light text-gray-700 mt-2.5 dark:text-gray-200">
Pay attention not to insert an existing keyword, or you will
overwrite that keyword
{i18n.t(
"Pay attention not to insert an existing keyword, or you will overwrite that keyword"
)}
</label>
<textarea
rows={3}
className="t-textarea mt-2"
placeholder="Welcome"
placeholder={i18n.t("Context")}
value={key}
onChange={handleChangeKey}
></textarea>
<Button
className="mt-2"
onClick={saveKey}
variant={isWarning ? "warning" : "default"}
>
{isWarning ? "Overwrite the keyword" : "Change"}
</Button>
<div className="flex gap-2">
<Button
className="mt-2"
onClick={saveKey}
variant={isWarning ? "warning" : "default"}
>
{isWarning ? "Overwrite the keyword" : "Change"}
</Button>
<Button className="mt-2" onClick={askAI} variant={"default"}>
{i18n.t("Generate")}
</Button>
</div>
</div>
</SlideOverRow>
<SlideOverRow title="Description">
<SlideOverRow title="Context">
<div className="mt-1">
<textarea
rows={3}
className="t-textarea"
placeholder="Leave the keyword description here..."
placeholder="Leave the keyword context here..."
value={keyword.info?.context}
onChange={handleChangeContext}
></textarea>
Expand Down
4 changes: 4 additions & 0 deletions components/app/project/table/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useCallback, useMemo, useState } from "react"
import i18n from "@/lib/i18n"

import AddNewKeyword, { NewKeyword } from "../dialogs/add-new-keyword"
import { ProjectData } from "../types"
import { Keyword } from "../useTranslation"
import DetailSlideOver from "./detail-slide-over"
import Row from "./row"
Expand All @@ -18,6 +19,7 @@ type Props = {
editContext: (key: string, context: string) => void
editKey: (key: string, newKey: string) => void
checkIfKeyAlreadyExists: (key: string) => boolean
project: ProjectData
}

const Table = (props: Props) => {
Expand All @@ -30,6 +32,7 @@ const Table = (props: Props) => {
editContext,
editKey,
checkIfKeyAlreadyExists,
project,
} = props

const [keySelected, selectKey] = useState<string | undefined>(undefined)
Expand Down Expand Up @@ -126,6 +129,7 @@ const Table = (props: Props) => {
</div>
{keywordSelected && (
<DetailSlideOver
project={project}
onClose={closeDetailRow}
keyword={keywordSelected}
editTranslation={editTranslation}
Expand Down
6 changes: 6 additions & 0 deletions components/app/project/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Project } from "@prisma/client"

export type ProjectData = Pick<
Project,
"id" | "title" | "languages" | "published" | "info" | "settings"
>
8 changes: 6 additions & 2 deletions lib/i18n/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,9 @@
"Upload .json file": "Upload .json file",
"Use a glossary to keep project translations consistent": "Use a glossary to keep project translations consistent",
"User wrong": "User wrong",
"Write the word you want to be preferred over other similar words in the languages you prefer": "Write the word you want to be preferred over other similar words in the languages you prefer"
}
"Write the word you want to be preferred over other similar words in the languages you prefer": "Write the word you want to be preferred over other similar words in the languages you prefer",
"Pay attention not to insert an existing keyword, or you will overwrite that keyword": "Pay attention not to insert an existing keyword, or you will overwrite that keyword",
"Generate": "Generate",
"The project does not exist or is not published": "The project does not exist or is not published",
"The project does not have any languages": "The project does not have any languages"
}
2 changes: 1 addition & 1 deletion store/useI18nState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export const useI18nState = create<I18nState>()(
i18n: {
...state.i18n,
languages: state.i18n.languages.map((_language) => {
if (_language.lang !== language) {
if (_language.lang !== language && _language.short !== language) {
return _language
}

Expand Down
4 changes: 2 additions & 2 deletions utils/OpenAiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class OpenAIHelper {
maxTokens?: number
}) =>
this.openai.chat.completions.create({
model: "gpt-3.5-turbo",
model: "gpt-4-vision-preview",
max_tokens: maxTokens,
messages: [
{
Expand Down Expand Up @@ -59,7 +59,7 @@ export class OpenAIHelper {
maxTokens?: number
}) =>
this.openai.chat.completions.create({
model: "gpt-4-vision-preview",
model: "gpt-3.5-turbo",
max_tokens: maxTokens,
messages: [
{
Expand Down
Loading