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

implement language as state instead of cookie #1785

Merged
merged 5 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 3 additions & 6 deletions website/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"next/core-web-vitals"
],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "next/core-web-vitals"],
"rules": {
"unused-imports/no-unused-imports": "warn",
"simple-import-sort/imports": "warn",
Expand All @@ -18,7 +14,8 @@
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
]
],
"@typescript-eslint/no-non-null-assertion": 0
notmd marked this conversation as resolved.
Show resolved Hide resolved
},
"plugins": ["simple-import-sort", "unused-imports"]
}
2 changes: 1 addition & 1 deletion website/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Cypress.Commands.add("signInUsingEmailedLink", (emailAddress) => {
// we do a GET to this url to force the python backend to add an entry for our user
// in the database, otherwise the tos acceptance will error with 404 user not found
// then we accept the tos
cy.request("GET", "/api/available_tasks").then(() => cy.request("POST", "/api/tos", {}));
cy.request("GET", "/api/available_tasks?lang=en").then(() => cy.request("POST", "/api/tos", {}));
});
});

Expand Down
5 changes: 2 additions & 3 deletions website/src/components/Messages/LabelFlagGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button, Flex, Tooltip } from "@chakra-ui/react";
import { useTranslation } from "next-i18next";
import { useCookies } from "react-cookie";
import { useCurrentLocale } from "src/hooks/locale/useCurrentLocale";
import { getTypeSafei18nKey } from "src/lib/i18n";
import { getLocaleDisplayName } from "src/lib/languages";

Expand All @@ -20,8 +20,7 @@ export const LabelFlagGroup = ({
onChange,
}: LabelFlagGroupProps) => {
const { t } = useTranslation("labelling");
const [cookies] = useCookies(["NEXT_LOCALE"]);
const currentLanguage = cookies["NEXT_LOCALE"];
const currentLanguage = useCurrentLocale();
const expectedLanguageName = getLocaleDisplayName(expectedLanguage, currentLanguage);
return (
<Flex wrap="wrap" gap="4">
Expand Down
3 changes: 3 additions & 0 deletions website/src/hooks/locale/useCurrentLocale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { useRouter } from "next/router";

export const useCurrentLocale = () => useRouter().locale || "en";
16 changes: 10 additions & 6 deletions website/src/hooks/tasks/useGenericTaskAPI.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import { useCallback, useState } from "react";
import { TaskInfos } from "src/components/Tasks/TaskTypes";
import { get, post } from "src/lib/api";
import { API_ROUTES } from "src/lib/routes";
import { TaskApiHook } from "src/types/Hooks";
import { BaseTask, ServerTaskResponse, TaskResponse, TaskType as TaskTypeEnum } from "src/types/Task";
import { AllTaskReplies } from "src/types/TaskResponses";
import useSWRImmutable from "swr/immutable";
import useSWRMutation from "swr/mutation";

import { useCurrentLocale } from "../locale/useCurrentLocale";

export const useGenericTaskAPI = <TaskType extends BaseTask, ResponseContent = AllTaskReplies>(
taskType: TaskTypeEnum
): TaskApiHook<TaskType, ResponseContent> => {
const [response, setResponse] = useState<TaskResponse<TaskType>>({ taskAvailability: "AWAITING_INITIAL" });

const locale = useCurrentLocale();
// Note: We use isValidating to indicate we are loading because it signals eash load, not just the first one.
const { isValidating: isLoading, mutate: requestNewTask } = useSWRImmutable<ServerTaskResponse<TaskType>>(
"/api/new_task/" + taskType,
API_ROUTES.NEW_TASK(taskType, { lang: locale }),
get,
{
onSuccess: (taskResponse) => {
setResponse({
...taskResponse,
taskAvailability: "AVAILABLE",
taskInfo: TaskInfos.find((taskType) => taskType.type === taskResponse.task.type),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
taskInfo: TaskInfos.find((taskType) => taskType.type === taskResponse.task.type)!,
});
},
onError: () => {
Expand All @@ -33,7 +37,7 @@ export const useGenericTaskAPI = <TaskType extends BaseTask, ResponseContent = A
}
);

const { trigger: sendTaskContent } = useSWRMutation("/api/update_task", post, {
const { trigger: sendTaskContent } = useSWRMutation(API_ROUTES.UPDATE_TASK, post, {
onSuccess: () => {
requestNewTask();
},
Expand Down Expand Up @@ -63,9 +67,9 @@ export const useGenericTaskAPI = <TaskType extends BaseTask, ResponseContent = A
if (response.taskAvailability !== "AVAILABLE") {
throw new Error("Cannot complete task that is not yet ready");
}
await sendTaskContent({ id: response.id, update_type: response.taskInfo.update_type, content });
await sendTaskContent({ id: response.id, update_type: response.taskInfo.update_type, content, lang: locale });
},
[response, sendTaskContent]
[response, sendTaskContent, locale]
);

return { response, isLoading, rejectTask, completeTask };
Expand Down
23 changes: 22 additions & 1 deletion website/src/lib/languages.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { NextApiRequest } from "next";

import { OasstError } from "./oasst_api_client";

const missingDisplayNamesForLocales = {
eu: "Euskara",
gl: "Galego",
Expand All @@ -6,7 +10,7 @@ const missingDisplayNamesForLocales = {
/**
* Returns the locale's name.
*/
export const getLocaleDisplayName = (locale: string, displayLocale = undefined) => {
export const getLocaleDisplayName = (locale: string, displayLocale?: string) => {
// Intl defaults to English for locales that are not oficially translated
if (missingDisplayNamesForLocales[locale]) {
return missingDisplayNamesForLocales[locale];
Expand All @@ -15,3 +19,20 @@ export const getLocaleDisplayName = (locale: string, displayLocale = undefined)
// Return the Titlecased version of the language name.
return displayName.charAt(0).toLocaleUpperCase() + displayName.slice(1);
};

export const getLanguageFromRequest = (req: NextApiRequest) => {
const body = req.method === "GET" ? req.query : req.body;
const lang = body["lang"];

if (!lang || typeof lang !== "string") {
throw new OasstError({
message: "Invalid language",
httpStatusCode: -1,
errorCode: -1,
path: req.url!,
method: req.method!,
});
}

return lang;
};
42 changes: 42 additions & 0 deletions website/src/lib/routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,46 @@
import { TaskType } from "src/types/Task";

export type RouteQuery = Record<string, string | number | boolean | undefined>;

export const stringifyQuery = (query: RouteQuery | undefined) => {
if (!query) {
return "";
}

const filteredQuery = Object.fromEntries(Object.entries(query).filter(([, value]) => value !== undefined)) as Record<
string,
string
>;

return new URLSearchParams(filteredQuery).toString();
};

const createRoute = (path: string, query?: RouteQuery) => {
if (!query) {
return path;
}

return `${path}?${stringifyQuery(query)}`;
};

export const ROUTES = {
ADMIN_MESSAGE_DETAIL: (id: string) => `/admin/messages/${id}`,
MESSAGE_DETAIL: (id: string) => `/messages/${id}`,
};

export type QueryWithLang<T extends RouteQuery | undefined = undefined> = T extends undefined
? { lang: string }
: T & { lang: string };

const withLang =
<T extends RouteQuery | undefined = undefined>(path: string, q?: T) =>
(query: QueryWithLang<T>) => {
return createRoute(path, { ...q, ...query });
};

export const API_ROUTES = {
NEW_TASK: (type: TaskType, query: QueryWithLang) => createRoute(`/api/new_task/${type}`, query),
UPDATE_TASK: "/api/update_task",
AVAILABLE_TASK: withLang("/api/available_tasks"),
RECENT_MESSAGES: withLang("/api/messages"),
};
26 changes: 0 additions & 26 deletions website/src/lib/users.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,6 @@
import parser from "accept-language-parser";
import type { NextApiRequest } from "next";
import { i18n } from "src/../next-i18next.config";
import prisma from "src/lib/prismadb";
import type { BackendUserCore } from "src/types/Users";

const LOCALE_SET = new Set(i18n.locales);

/**
* Returns the most appropriate user language using the following priority:
*
* 1. The `NEXT_LOCALE` cookie which is set by the client side and will be in
* the set of supported locales.
* 2. The `accept-language` header if it contains a supported locale as set by
* the i18n module.
* 3. "en" as a final fallback.
*/
export const getUserLanguage = (req: NextApiRequest): string => {
const cookieLanguage = req.cookies["NEXT_LOCALE"];
if (cookieLanguage) {
return cookieLanguage;
}
const headerLanguages = parser.parse(req.headers["accept-language"]);
if (headerLanguages.length > 0 && LOCALE_SET.has(headerLanguages[0].code)) {
return headerLanguages[0].code;
}
return "en";
};

/**
* Returns a `BackendUserCore` that can be used for interacting with the Backend service.
*
Expand Down
9 changes: 5 additions & 4 deletions website/src/pages/api/available_tasks.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { withoutRole } from "src/lib/auth";
import { getLanguageFromRequest } from "src/lib/languages";
import { createApiClientFromUser } from "src/lib/oasst_client_factory";
import { getBackendUserCore, getUserLanguage } from "src/lib/users";
import { getBackendUserCore } from "src/lib/users";

const handler = withoutRole("banned", async (req, res, token) => {
const user = await getBackendUserCore(token.sub);
const oasstApiClient = createApiClientFromUser(user);
const userLanguage = getUserLanguage(req);
const availableTasks = await oasstApiClient.fetch_available_tasks(user, userLanguage);
const oasstApiClient = createApiClientFromUser(user!);
const userLanguage = getLanguageFromRequest(req);
const availableTasks = await oasstApiClient.fetch_available_tasks(user!, userLanguage);
res.status(200).json(availableTasks);
});

Expand Down
4 changes: 2 additions & 2 deletions website/src/pages/api/messages/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { withoutRole } from "src/lib/auth";
import { getLanguageFromRequest } from "src/lib/languages";
import { createApiClient } from "src/lib/oasst_client_factory";
import { getUserLanguage } from "src/lib/users";

const handler = withoutRole("banned", async (req, res, token) => {
const client = await createApiClient(token);
const userLanguage = getUserLanguage(req);
const userLanguage = getLanguageFromRequest(req);
const messages = await client.fetch_recent_messages(userLanguage);
res.status(200).json(messages);
});
Expand Down
11 changes: 6 additions & 5 deletions website/src/pages/api/new_task/[task_type].ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { withoutRole } from "src/lib/auth";
import { ERROR_CODES } from "src/lib/constants";
import { getLanguageFromRequest } from "src/lib/languages";
import { OasstError } from "src/lib/oasst_api_client";
import { createApiClientFromUser } from "src/lib/oasst_client_factory";
import prisma from "src/lib/prismadb";
import { getBackendUserCore, getUserLanguage } from "src/lib/users";
import { getBackendUserCore } from "src/lib/users";

/**
* Returns a new task created from the Task Backend. We do a few things here:
Expand All @@ -16,16 +17,16 @@ import { getBackendUserCore, getUserLanguage } 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 lang = getLanguageFromRequest(req);

const user = await getBackendUserCore(token.sub);
const oasstApiClient = createApiClientFromUser(user);
const oasstApiClient = createApiClientFromUser(user!);
let task;
try {
task = await oasstApiClient.fetchTask(task_type as string, user, userLanguage);
task = await oasstApiClient.fetchTask(task_type as string, user!, lang);
} catch (err) {
if (err instanceof OasstError && err.errorCode === ERROR_CODES.TASK_REQUESTED_TYPE_NOT_AVAILABLE) {
res.status(503).json({});
res.status(503).json(err);
} else {
console.error(err);
res.status(500).json(err);
Expand Down
16 changes: 5 additions & 11 deletions website/src/pages/api/update_task.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Prisma } from "@prisma/client";
import { withoutRole } from "src/lib/auth";
import { getLanguageFromRequest } from "src/lib/languages";
import { createApiClient } from "src/lib/oasst_client_factory";
import prisma from "src/lib/prismadb";
import { getBackendUserCore, getUserLanguage } from "src/lib/users";
import { getBackendUserCore } from "src/lib/users";

/**
* Stores the task interaction with the Task Backend and then returns the next task generated.
Expand All @@ -18,6 +19,8 @@ const handler = withoutRole("banned", async (req, res, token) => {
// Parse out the local task ID and the interaction contents.
const { id: frontendId, content, update_type } = req.body;

const lang = getLanguageFromRequest(req);

// do in parallel since they are independent
const [_, registeredTask, oasstApiClient] = await Promise.all([
// Record that the user has done meaningful work and is no longer new.
Expand Down Expand Up @@ -46,18 +49,9 @@ 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,
userLanguage
);
newTask = await oasstApiClient.interactTask(update_type, taskId, frontendId, interaction.id, content, user!, lang);
} catch (err) {
console.error(JSON.stringify(err));
return res.status(500).json(err);
Expand Down
22 changes: 6 additions & 16 deletions website/src/pages/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,24 @@
import { Flex } from "@chakra-ui/react";
import Head from "next/head";
import { useTranslation } from "next-i18next";
import { useEffect, useMemo, useState } from "react";
import { useMemo } from "react";
import { LeaderboardWidget, TaskOption, WelcomeCard } from "src/components/Dashboard";
import { getDashboardLayout } from "src/components/Layout";
import { get } from "src/lib/api";
import { AvailableTasks, TaskCategory } from "src/types/Task";
export { getDefaultStaticProps as getStaticProps } from "src/lib/default_static_props";
import { TaskCategoryItem } from "src/components/Dashboard/TaskOption";
import { useCurrentLocale } from "src/hooks/locale/useCurrentLocale";
import { API_ROUTES } from "src/lib/routes";
import useSWR from "swr";

const Dashboard = () => {
const {
t,
i18n: { language },
} = useTranslation(["dashboard", "common", "tasks"]);
const [activeLang, setLang] = useState<string>(null);
const { data, mutate: fetchTasks } = useSWR<AvailableTasks>("/api/available_tasks", get, {
const { t } = useTranslation(["dashboard", "common", "tasks"]);
const lang = useCurrentLocale();
const { data } = useSWR<AvailableTasks>(API_ROUTES.AVAILABLE_TASK({ lang }), get, {
refreshInterval: 2 * 60 * 1000, //2 minutes
revalidateOnMount: false, // triggered in the hook below
});

useEffect(() => {
// re-fetch tasks if the language has changed
if (activeLang !== language) {
setLang(language);
fetchTasks();
}
}, [activeLang, setLang, language, fetchTasks]);

AbdBarho marked this conversation as resolved.
Show resolved Hide resolved
const availableTaskTypes = useMemo(() => {
const taskTypes = filterAvailableTasks(data ?? {});
return { [TaskCategory.Random]: taskTypes };
Expand Down