chore: implement i18n setting

This commit is contained in:
steven
2023-09-04 23:41:41 +08:00
parent a49a708fc5
commit 4f0a8cdc0a
27 changed files with 957 additions and 380 deletions

View File

@ -225,6 +225,64 @@ export declare class CreateUserResponse extends Message<CreateUserResponse> {
static equals(a: CreateUserResponse | PlainMessage<CreateUserResponse> | undefined, b: CreateUserResponse | PlainMessage<CreateUserResponse> | undefined): boolean;
}
/**
* @generated from message slash.api.v2.UpdateUserRequest
*/
export declare class UpdateUserRequest extends Message<UpdateUserRequest> {
/**
* @generated from field: int32 id = 1;
*/
id: number;
/**
* @generated from field: slash.api.v2.User user = 2;
*/
user?: User;
/**
* @generated from field: repeated string update_mask = 3;
*/
updateMask: string[];
constructor(data?: PartialMessage<UpdateUserRequest>);
static readonly runtime: typeof proto3;
static readonly typeName = "slash.api.v2.UpdateUserRequest";
static readonly fields: FieldList;
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UpdateUserRequest;
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UpdateUserRequest;
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UpdateUserRequest;
static equals(a: UpdateUserRequest | PlainMessage<UpdateUserRequest> | undefined, b: UpdateUserRequest | PlainMessage<UpdateUserRequest> | undefined): boolean;
}
/**
* @generated from message slash.api.v2.UpdateUserResponse
*/
export declare class UpdateUserResponse extends Message<UpdateUserResponse> {
/**
* @generated from field: slash.api.v2.User user = 1;
*/
user?: User;
constructor(data?: PartialMessage<UpdateUserResponse>);
static readonly runtime: typeof proto3;
static readonly typeName = "slash.api.v2.UpdateUserResponse";
static readonly fields: FieldList;
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UpdateUserResponse;
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UpdateUserResponse;
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UpdateUserResponse;
static equals(a: UpdateUserResponse | PlainMessage<UpdateUserResponse> | undefined, b: UpdateUserResponse | PlainMessage<UpdateUserResponse> | undefined): boolean;
}
/**
* @generated from message slash.api.v2.DeleteUserRequest
*/

View File

@ -93,6 +93,28 @@ export const CreateUserResponse = proto3.makeMessageType(
],
);
/**
* @generated from message slash.api.v2.UpdateUserRequest
*/
export const UpdateUserRequest = proto3.makeMessageType(
"slash.api.v2.UpdateUserRequest",
() => [
{ no: 1, name: "id", kind: "scalar", T: 5 /* ScalarType.INT32 */ },
{ no: 2, name: "user", kind: "message", T: User },
{ no: 3, name: "update_mask", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
],
);
/**
* @generated from message slash.api.v2.UpdateUserResponse
*/
export const UpdateUserResponse = proto3.makeMessageType(
"slash.api.v2.UpdateUserResponse",
() => [
{ no: 1, name: "user", kind: "message", T: User },
],
);
/**
* @generated from message slash.api.v2.DeleteUserRequest
*/

View File

@ -3,7 +3,7 @@
/* eslint-disable */
// @ts-nocheck
import type { BinaryReadOptions, FieldList, FieldMask, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3 } from "@bufbuild/protobuf";
/**
@ -128,11 +128,11 @@ export declare class UpdateUserSettingRequest extends Message<UpdateUserSettingR
userSetting?: UserSetting;
/**
* update_mask is the field mask to update the user setting.
* update_mask is the field mask to update.
*
* @generated from field: google.protobuf.FieldMask update_mask = 3;
* @generated from field: repeated string update_mask = 3;
*/
updateMask?: FieldMask;
updateMask: string[];
constructor(data?: PartialMessage<UpdateUserSettingRequest>);

View File

@ -3,7 +3,7 @@
/* eslint-disable */
// @ts-nocheck
import { FieldMask, proto3 } from "@bufbuild/protobuf";
import { proto3 } from "@bufbuild/protobuf";
/**
* @generated from message slash.api.v2.UserSetting
@ -56,7 +56,7 @@ export const UpdateUserSettingRequest = proto3.makeMessageType(
() => [
{ no: 1, name: "id", kind: "scalar", T: 5 /* ScalarType.INT32 */ },
{ no: 2, name: "user_setting", kind: "message", T: UserSetting },
{ no: 3, name: "update_mask", kind: "message", T: FieldMask },
{ no: 3, name: "update_mask", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
],
);

View File

@ -7,7 +7,8 @@
"create": "Create",
"download": "Download",
"edit": "Edit",
"delete": "Delete"
"delete": "Delete",
"language": "Language"
},
"analytics": {
"self": "Analytics",
@ -35,4 +36,4 @@
}
}
}
}
}

View File

@ -7,7 +7,8 @@
"create": "创建",
"download": "下载",
"edit": "编辑",
"delete": "删除"
"delete": "删除",
"language": "语言"
},
"analytics": {
"self": "分析",
@ -35,4 +36,4 @@
}
}
}
}
}

View File

@ -0,0 +1,58 @@
import { Option, Select } from "@mui/joy";
import { useTranslation } from "react-i18next";
import { UserSetting, UserSetting_Locale } from "@/types/proto/api/v2/user_setting_service_pb";
import useUserStore from "../../stores/v1/user";
const PreferenceSection: React.FC = () => {
const { t } = useTranslation();
const userStore = useUserStore();
const userSetting = userStore.getCurrentUserSetting();
const language = userSetting.locale || UserSetting_Locale.EN;
const languageOptions = [
{
value: "LOCALE_EN",
label: "English",
},
{
value: "LOCALE_ZH",
label: "中文",
},
];
const handleSelectLanguage = async (locale: UserSetting_Locale) => {
if (!locale) {
return;
}
await userStore.updateUserSetting(
{
...userSetting,
locale: locale,
} as UserSetting,
["locale"]
);
};
return (
<>
<div className="w-full flex flex-col justify-start items-start gap-y-2">
<p className="text-base font-semibold leading-6 text-gray-900">Preference</p>
<div className="w-full flex flex-row justify-between items-center">
<span>{t("common.language")}</span>
<Select defaultValue={language} onChange={(_, value) => handleSelectLanguage(value as UserSetting_Locale)}>
{languageOptions.map((option) => {
return (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
);
})}
</Select>
</div>
</div>
</>
);
};
export default PreferenceSection;

View File

@ -1,12 +1,16 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en from "../../locales/en.json";
import zh from "../../locales/zh.json";
i18n.use(initReactI18next).init({
resources: {
en: {
translation: en,
},
zh: {
translation: zh,
},
},
lng: "en",
fallbackLng: "en",

View File

@ -1,7 +1,7 @@
import { isEqual } from "lodash-es";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Outlet, useNavigate } from "react-router-dom";
import { UserSetting_Locale } from "@/types/proto/api/v2/user_setting_service_pb";
import Header from "../components/Header";
import useUserStore from "../stores/v1/user";
@ -11,6 +11,7 @@ const Root: React.FC = () => {
const userStore = useUserStore();
const currentUser = userStore.getCurrentUser();
const currentUserSetting = userStore.getCurrentUserSetting();
const isInitialized = Boolean(currentUser) && Boolean(currentUserSetting);
useEffect(() => {
if (!currentUser) {
@ -29,16 +30,16 @@ const Root: React.FC = () => {
return;
}
if (currentUserSetting.locale === UserSetting_Locale.EN) {
if (isEqual(currentUserSetting.locale, "LOCALE_EN")) {
i18n.changeLanguage("en");
} else if (currentUserSetting.locale === UserSetting_Locale.ZH) {
} else if (isEqual(currentUserSetting.locale, "LOCALE_ZH")) {
i18n.changeLanguage("zh");
}
}, [currentUserSetting]);
return (
<>
{currentUser && (
{isInitialized && (
<div className="w-full h-auto flex flex-col justify-start items-start">
<Header />
<Outlet />

View File

@ -1,38 +0,0 @@
{
"common": {
"about": "About",
"loading": "Loading",
"cancel": "Cancel",
"save": "Save",
"create": "Create",
"download": "Download",
"edit": "Edit",
"delete": "Delete"
},
"analytics": {
"self": "Analytics",
"top-sources": "Top sources",
"source": "Source",
"visitors": "Visitors",
"devices": "Devices",
"browser": "Browser",
"browsers": "Browsers",
"operating-system": "Operating System"
},
"shortcut": {
"visibility": {
"private": {
"self": "Private",
"description": "Only you can access"
},
"workspace": {
"self": "Workspace",
"description": "Workspace members can access"
},
"public": {
"self": "Public",
"description": "Visible to everyone on the internet"
}
}
}
}

View File

@ -1,38 +0,0 @@
{
"common": {
"about": "关于",
"loading": "加载中",
"cancel": "取消",
"save": "保存",
"create": "创建",
"download": "下载",
"edit": "编辑",
"delete": "删除"
},
"analytics": {
"self": "分析",
"top-sources": "热门来源",
"source": "来源",
"visitors": "访客数",
"devices": "设备",
"browser": "浏览器",
"browsers": "浏览器",
"operating-system": "操作系统"
},
"shortcut": {
"visibility": {
"private": {
"self": "私有的",
"description": "仅您可以访问"
},
"workspace": {
"self": "工作区",
"description": "工作区成员可以访问"
},
"public": {
"self": "公开的",
"description": "对任何人可见"
}
}
}
}

View File

@ -1,3 +1,4 @@
import PreferenceSection from "@/components/setting/PreferenceSection";
import AccessTokenSection from "../components/setting/AccessTokenSection";
import AccountSection from "../components/setting/AccountSection";
import MemberSection from "../components/setting/MemberSection";
@ -12,6 +13,7 @@ const Setting: React.FC = () => {
<div className="mx-auto max-w-6xl w-full px-3 md:px-12 py-6 flex flex-col justify-start items-start gap-y-12">
<AccountSection />
<AccessTokenSection />
<PreferenceSection />
{isAdmin && (
<>
<MemberSection />

View File

@ -1,6 +1,6 @@
import axios from "axios";
import { create } from "zustand";
import { GetUserSettingResponse, UserSetting } from "@/types/proto/api/v2/user_setting_service_pb";
import { GetUserSettingResponse, UpdateUserSettingResponse, UserSetting } from "@/types/proto/api/v2/user_setting_service_pb";
import * as api from "../../helpers/api";
const convertResponseModelUser = (user: User): User => {
@ -28,6 +28,7 @@ interface UserState {
// User setting related actions.
fetchUserSetting: (userId: UserId) => Promise<UserSetting>;
updateUserSetting: (userSetting: UserSetting, updateMask: string[]) => Promise<UserSetting>;
getCurrentUserSetting: () => UserSetting;
}
@ -105,6 +106,23 @@ const useUserStore = create<UserState>()((set, get) => ({
set(userSettingMap);
return userSetting;
},
updateUserSetting: async (userSetting: UserSetting, updateMask: string[]) => {
const userId = userSetting.id;
const {
data: { userSetting: updatedUserSetting },
} = await axios.post<UpdateUserSettingResponse>(`api/v2/users/${userId}/settings`, {
id: userId,
userSetting,
updateMask,
});
if (!updatedUserSetting) {
throw new Error(`User setting not found for user ${userId}`);
}
const userSettingMap = get().userSettingMapById;
userSettingMap[userId] = updatedUserSetting;
set(userSettingMap);
return updatedUserSetting;
},
getCurrentUserSetting: () => {
const userSettingMap = get().userSettingMapById;
const currentUserId = get().currentUserId;

View File

@ -225,6 +225,64 @@ export declare class CreateUserResponse extends Message<CreateUserResponse> {
static equals(a: CreateUserResponse | PlainMessage<CreateUserResponse> | undefined, b: CreateUserResponse | PlainMessage<CreateUserResponse> | undefined): boolean;
}
/**
* @generated from message slash.api.v2.UpdateUserRequest
*/
export declare class UpdateUserRequest extends Message<UpdateUserRequest> {
/**
* @generated from field: int32 id = 1;
*/
id: number;
/**
* @generated from field: slash.api.v2.User user = 2;
*/
user?: User;
/**
* @generated from field: repeated string update_mask = 3;
*/
updateMask: string[];
constructor(data?: PartialMessage<UpdateUserRequest>);
static readonly runtime: typeof proto3;
static readonly typeName = "slash.api.v2.UpdateUserRequest";
static readonly fields: FieldList;
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UpdateUserRequest;
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UpdateUserRequest;
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UpdateUserRequest;
static equals(a: UpdateUserRequest | PlainMessage<UpdateUserRequest> | undefined, b: UpdateUserRequest | PlainMessage<UpdateUserRequest> | undefined): boolean;
}
/**
* @generated from message slash.api.v2.UpdateUserResponse
*/
export declare class UpdateUserResponse extends Message<UpdateUserResponse> {
/**
* @generated from field: slash.api.v2.User user = 1;
*/
user?: User;
constructor(data?: PartialMessage<UpdateUserResponse>);
static readonly runtime: typeof proto3;
static readonly typeName = "slash.api.v2.UpdateUserResponse";
static readonly fields: FieldList;
static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): UpdateUserResponse;
static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): UpdateUserResponse;
static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): UpdateUserResponse;
static equals(a: UpdateUserResponse | PlainMessage<UpdateUserResponse> | undefined, b: UpdateUserResponse | PlainMessage<UpdateUserResponse> | undefined): boolean;
}
/**
* @generated from message slash.api.v2.DeleteUserRequest
*/

View File

@ -93,6 +93,28 @@ export const CreateUserResponse = proto3.makeMessageType(
],
);
/**
* @generated from message slash.api.v2.UpdateUserRequest
*/
export const UpdateUserRequest = proto3.makeMessageType(
"slash.api.v2.UpdateUserRequest",
() => [
{ no: 1, name: "id", kind: "scalar", T: 5 /* ScalarType.INT32 */ },
{ no: 2, name: "user", kind: "message", T: User },
{ no: 3, name: "update_mask", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
],
);
/**
* @generated from message slash.api.v2.UpdateUserResponse
*/
export const UpdateUserResponse = proto3.makeMessageType(
"slash.api.v2.UpdateUserResponse",
() => [
{ no: 1, name: "user", kind: "message", T: User },
],
);
/**
* @generated from message slash.api.v2.DeleteUserRequest
*/

View File

@ -3,7 +3,7 @@
/* eslint-disable */
// @ts-nocheck
import type { BinaryReadOptions, FieldList, FieldMask, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3 } from "@bufbuild/protobuf";
/**
@ -128,11 +128,11 @@ export declare class UpdateUserSettingRequest extends Message<UpdateUserSettingR
userSetting?: UserSetting;
/**
* update_mask is the field mask to update the user setting.
* update_mask is the field mask to update.
*
* @generated from field: google.protobuf.FieldMask update_mask = 3;
* @generated from field: repeated string update_mask = 3;
*/
updateMask?: FieldMask;
updateMask: string[];
constructor(data?: PartialMessage<UpdateUserSettingRequest>);

View File

@ -3,7 +3,7 @@
/* eslint-disable */
// @ts-nocheck
import { FieldMask, proto3 } from "@bufbuild/protobuf";
import { proto3 } from "@bufbuild/protobuf";
/**
* @generated from message slash.api.v2.UserSetting
@ -56,7 +56,7 @@ export const UpdateUserSettingRequest = proto3.makeMessageType(
() => [
{ no: 1, name: "id", kind: "scalar", T: 5 /* ScalarType.INT32 */ },
{ no: 2, name: "user_setting", kind: "message", T: UserSetting },
{ no: 3, name: "update_mask", kind: "message", T: FieldMask },
{ no: 3, name: "update_mask", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true },
],
);