diff --git a/website/next-i18next.config.js b/website/next-i18next.config.js
index 7c87a7a4..40c4b14d 100644
--- a/website/next-i18next.config.js
+++ b/website/next-i18next.config.js
@@ -1,6 +1,6 @@
module.exports = {
i18n: {
defaultLocale: "en",
- locales: ["en"],
+ locales: ["de", "en", "fr"],
},
};
diff --git a/website/package-lock.json b/website/package-lock.json
index 06f3c98d..5c5dc795 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -21,6 +21,7 @@
"@next/font": "^13.1.0",
"@prisma/client": "^4.7.1",
"@tailwindcss/forms": "^0.5.3",
+ "accept-language-parser": "^1.5.0",
"autoprefixer": "^10.4.13",
"axios": "^1.2.1",
"boolean": "^3.2.0",
@@ -38,6 +39,7 @@
"npm": "^9.2.0",
"postcss-focus-visible": "^7.1.0",
"react": "18.2.0",
+ "react-cookies": "^0.1.1",
"react-dom": "18.2.0",
"react-feature-flags": "^1.0.0",
"react-hook-form": "^7.42.1",
@@ -13616,6 +13618,11 @@
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
"dev": true
},
+ "node_modules/accept-language-parser": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz",
+ "integrity": "sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw=="
+ },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -32466,6 +32473,23 @@
"react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-cookies": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/react-cookies/-/react-cookies-0.1.1.tgz",
+ "integrity": "sha512-PP75kJ4vtoHuuTdq0TAD3RmlAv7vuDQh9fkC4oDlhntgs9vX1DmREomO0Y1mcQKR9nMZ6/zxoflaMJ3MAmF5KQ==",
+ "dependencies": {
+ "cookie": "^0.3.1",
+ "object-assign": "^4.1.1"
+ }
+ },
+ "node_modules/react-cookies/node_modules/cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/react-docgen": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
@@ -47817,6 +47841,11 @@
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
"dev": true
},
+ "accept-language-parser": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz",
+ "integrity": "sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw=="
+ },
"accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -61962,6 +61991,22 @@
"@babel/runtime": "^7.12.13"
}
},
+ "react-cookies": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/react-cookies/-/react-cookies-0.1.1.tgz",
+ "integrity": "sha512-PP75kJ4vtoHuuTdq0TAD3RmlAv7vuDQh9fkC4oDlhntgs9vX1DmREomO0Y1mcQKR9nMZ6/zxoflaMJ3MAmF5KQ==",
+ "requires": {
+ "cookie": "^0.3.1",
+ "object-assign": "^4.1.1"
+ },
+ "dependencies": {
+ "cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw=="
+ }
+ }
+ },
"react-docgen": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
diff --git a/website/package.json b/website/package.json
index 4ae762e4..8866a9e2 100644
--- a/website/package.json
+++ b/website/package.json
@@ -38,6 +38,7 @@
"@next/font": "^13.1.0",
"@prisma/client": "^4.7.1",
"@tailwindcss/forms": "^0.5.3",
+ "accept-language-parser": "^1.5.0",
"autoprefixer": "^10.4.13",
"axios": "^1.2.1",
"boolean": "^3.2.0",
@@ -55,6 +56,7 @@
"npm": "^9.2.0",
"postcss-focus-visible": "^7.1.0",
"react": "18.2.0",
+ "react-cookies": "^0.1.1",
"react-dom": "18.2.0",
"react-feature-flags": "^1.0.0",
"react-hook-form": "^7.42.1",
diff --git a/website/src/components/Header/Header.tsx b/website/src/components/Header/Header.tsx
index a1b36123..64614578 100644
--- a/website/src/components/Header/Header.tsx
+++ b/website/src/components/Header/Header.tsx
@@ -5,6 +5,7 @@ import { useSession } from "next-auth/react";
import { useTranslation } from "next-i18next";
import { Flags } from "react-feature-flags";
import { FaUser } from "react-icons/fa";
+import { LanguageSelector } from "src/components/LanguageSelector";
import { UserMenu } from "./UserMenu";
@@ -45,6 +46,7 @@ export function Header() {
FlagTest
+
diff --git a/website/src/components/LanguageSelector/LanguageSelector.tsx b/website/src/components/LanguageSelector/LanguageSelector.tsx
new file mode 100644
index 00000000..e611bf0f
--- /dev/null
+++ b/website/src/components/LanguageSelector/LanguageSelector.tsx
@@ -0,0 +1,40 @@
+import { Select } from "@chakra-ui/react";
+import { useRouter } from "next/router";
+import { useTranslation } from "next-i18next";
+import { useCallback, useMemo, useState } from "react";
+import cookie from "react-cookies";
+
+const LanguageSelector = () => {
+ const router = useRouter();
+ const { i18n } = useTranslation();
+
+ const { language: currentLanguage } = i18n;
+ const languageNames = useMemo(() => {
+ return new Intl.DisplayNames([currentLanguage], {
+ type: "language",
+ });
+ }, [currentLanguage]);
+
+ const languageChanged = useCallback(
+ async (option) => {
+ const locale = option.target.value;
+ cookie.save("NEXT_LOCALE", locale, { path: "/" });
+ const path = router.asPath;
+ return router.push(path, path, { locale });
+ },
+ [router]
+ );
+
+ const locales = router.locales;
+ return (
+
+ );
+};
+
+export { LanguageSelector };
diff --git a/website/src/components/LanguageSelector/index.tsx b/website/src/components/LanguageSelector/index.tsx
new file mode 100644
index 00000000..feb9f322
--- /dev/null
+++ b/website/src/components/LanguageSelector/index.tsx
@@ -0,0 +1 @@
+export * from "./LanguageSelector";
diff --git a/website/src/lib/oasst_api_client.ts b/website/src/lib/oasst_api_client.ts
index 1e74b020..36e3ae33 100644
--- a/website/src/lib/oasst_api_client.ts
+++ b/website/src/lib/oasst_api_client.ts
@@ -108,10 +108,11 @@ export class OasstApiClient {
// TODO return a strongly typed Task?
// This method is used to store a task in RegisteredTask.task.
// This is a raw Json type, so we can't use it to strongly type the task.
- async fetchTask(taskType: string, user: BackendUserCore): Promise {
+ async fetchTask(taskType: string, user: BackendUserCore, lang: string): Promise {
return this.post("/api/v1/tasks/", {
type: taskType,
user,
+ lang,
});
}
@@ -136,7 +137,8 @@ export class OasstApiClient {
messageId: string,
userMessageId: string,
content: object,
- user: BackendUserCore
+ user: BackendUserCore,
+ lang: string
): Promise {
return this.post("/api/v1/tasks/interaction", {
type: updateType,
@@ -144,6 +146,7 @@ export class OasstApiClient {
task_id: taskId,
message_id: messageId,
user_message_id: userMessageId,
+ lang,
...content,
});
}
diff --git a/website/src/lib/users.ts b/website/src/lib/users.ts
index 2aa8c708..3dbe5a08 100644
--- a/website/src/lib/users.ts
+++ b/website/src/lib/users.ts
@@ -1,6 +1,20 @@
+import parser from "accept-language-parser";
+import type { NextApiRequest } from "next";
import prisma from "src/lib/prismadb";
import type { BackendUserCore } from "src/types/Users";
+const getUserLanguage = (req: NextApiRequest) => {
+ const cookieLanguage = req.cookies["NEXT_LOCALE"];
+ if (cookieLanguage) {
+ return cookieLanguage;
+ }
+ const headerLanguages = parser.parse(req.headers["accept-language"]);
+ if (headerLanguages.length > 0) {
+ return headerLanguages[0].code;
+ }
+ return "en";
+};
+
/**
* Returns a `BackendUserCore` that can be used for interacting with the Backend service.
*
@@ -35,4 +49,4 @@ const getBackendUserCore = async (id: string) => {
} as BackendUserCore;
};
-export { getBackendUserCore };
+export { getBackendUserCore, getUserLanguage };
diff --git a/website/src/pages/api/new_task/[task_type].ts b/website/src/pages/api/new_task/[task_type].ts
index c8255b18..360b8faa 100644
--- a/website/src/pages/api/new_task/[task_type].ts
+++ b/website/src/pages/api/new_task/[task_type].ts
@@ -1,7 +1,7 @@
import { withoutRole } from "src/lib/auth";
import { oasstApiClient } from "src/lib/oasst_api_client";
import prisma from "src/lib/prismadb";
-import { getBackendUserCore } from "src/lib/users";
+import { getBackendUserCore, getUserLanguage } from "src/lib/users";
/**
* Returns a new task created from the Task Backend. We do a few things here:
@@ -14,11 +14,12 @@ import { getBackendUserCore } from "src/lib/users";
const handler = withoutRole("banned", async (req, res, token) => {
// Fetch the new task.
const { task_type } = req.query;
+ const userLanguage = getUserLanguage(req);
const user = await getBackendUserCore(token.sub);
let task;
try {
- task = await oasstApiClient.fetchTask(task_type as string, user);
+ task = await oasstApiClient.fetchTask(task_type as string, user, userLanguage);
} catch (err) {
console.error(err);
res.status(500).json(err);
diff --git a/website/src/pages/api/update_task.ts b/website/src/pages/api/update_task.ts
index c547503a..6f08d640 100644
--- a/website/src/pages/api/update_task.ts
+++ b/website/src/pages/api/update_task.ts
@@ -2,7 +2,7 @@ import { Prisma } from "@prisma/client";
import { withoutRole } from "src/lib/auth";
import { oasstApiClient } from "src/lib/oasst_api_client";
import prisma from "src/lib/prismadb";
-import { getBackendUserCore } from "src/lib/users";
+import { getBackendUserCore, getUserLanguage } from "src/lib/users";
/**
* Stores the task interaction with the Task Backend and then returns the next task generated.
@@ -41,9 +41,18 @@ const handler = withoutRole("banned", async (req, res, token) => {
});
const user = await getBackendUserCore(token.sub);
+ const userLanguage = getUserLanguage(req);
let newTask;
try {
- newTask = await oasstApiClient.interactTask(update_type, taskId, frontendId, interaction.id, content, user);
+ newTask = await oasstApiClient.interactTask(
+ update_type,
+ taskId,
+ frontendId,
+ interaction.id,
+ content,
+ user,
+ userLanguage
+ );
} catch (err) {
console.error(JSON.stringify(err));
return res.status(500).json(err);