mirror of
https://github.com/aykhans/slash-e.git
synced 2025-07-02 04:01:35 +00:00
chore: implement i18n setting
This commit is contained in:
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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>);
|
||||
|
||||
|
@ -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 },
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -7,7 +7,8 @@
|
||||
"create": "Create",
|
||||
"download": "Download",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete"
|
||||
"delete": "Delete",
|
||||
"language": "Language"
|
||||
},
|
||||
"analytics": {
|
||||
"self": "Analytics",
|
||||
@ -35,4 +36,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,8 @@
|
||||
"create": "创建",
|
||||
"download": "下载",
|
||||
"edit": "编辑",
|
||||
"delete": "删除"
|
||||
"delete": "删除",
|
||||
"language": "语言"
|
||||
},
|
||||
"analytics": {
|
||||
"self": "分析",
|
||||
@ -35,4 +36,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
58
frontend/web/src/components/setting/PreferenceSection.tsx
Normal file
58
frontend/web/src/components/setting/PreferenceSection.tsx
Normal 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;
|
@ -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",
|
||||
|
@ -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 />
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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": "对任何人可见"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 />
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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>);
|
||||
|
||||
|
@ -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 },
|
||||
],
|
||||
);
|
||||
|
||||
|
Reference in New Issue
Block a user