mirror of https://github.com/langgenius/dify.git
refactor(i18n): about locales
This commit is contained in:
parent
7a5d2728a1
commit
6bb12b341f
|
|
@ -8,7 +8,7 @@ import { noop } from 'es-toolkit/compat'
|
|||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import Picker from '@/app/components/base/date-and-time-picker/date-picker'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { formatToLocalTime } from '@/utils/format'
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ const DatePicker: FC<Props> = ({
|
|||
onStartChange,
|
||||
onEndChange,
|
||||
}) => {
|
||||
const { locale } = useI18N()
|
||||
const locale = useLocale()
|
||||
|
||||
const renderDate = useCallback(({ value, handleClickTrigger, isOpen }: TriggerProps) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import dayjs from 'dayjs'
|
|||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { HourglassShape } from '@/app/components/base/icons/src/vender/other'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { formatToLocalTime } from '@/utils/format'
|
||||
import DatePicker from './date-picker'
|
||||
import RangeSelector from './range-selector'
|
||||
|
|
@ -27,7 +27,7 @@ const TimeRangePicker: FC<Props> = ({
|
|||
onSelect,
|
||||
queryDateFormat,
|
||||
}) => {
|
||||
const { locale } = useI18N()
|
||||
const locale = useLocale()
|
||||
|
||||
const [isCustomRange, setIsCustomRange] = useState(false)
|
||||
const [start, setStart] = useState<Dayjs>(today)
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import I18NContext from '@/context/i18n'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { sendWebAppResetPasswordCode, verifyWebAppResetPasswordCode } from '@/service/common'
|
||||
|
||||
export default function CheckCode() {
|
||||
|
|
@ -19,7 +19,7 @@ export default function CheckCode() {
|
|||
const token = decodeURIComponent(searchParams.get('token') as string)
|
||||
const [code, setVerifyCode] = useState('')
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
|
||||
const verify = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import Link from 'next/link'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||
import { emailRegex } from '@/config'
|
||||
import I18NContext from '@/context/i18n'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { sendResetPasswordCode } from '@/service/common'
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ export default function CheckCode() {
|
|||
const router = useRouter()
|
||||
const [email, setEmail] = useState('')
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
|
||||
const handleGetEMailVerificationCode = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import I18NContext from '@/context/i18n'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common'
|
||||
import { fetchAccessToken } from '@/service/share'
|
||||
|
|
@ -23,7 +23,7 @@ export default function CheckCode() {
|
|||
const token = decodeURIComponent(searchParams.get('token') as string)
|
||||
const [code, setVerifyCode] = useState('')
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
const codeInputRef = useRef<HTMLInputElement>(null)
|
||||
const redirectUrl = searchParams.get('redirect_url')
|
||||
const embeddedUserId = useWebAppStore(s => s.embeddedUserId)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ import { noop } from 'es-toolkit/compat'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||
import { emailRegex } from '@/config'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { sendWebAppEMailLoginCode } from '@/service/common'
|
||||
|
||||
export default function MailAndCodeAuth() {
|
||||
|
|
@ -18,7 +17,7 @@ export default function MailAndCodeAuth() {
|
|||
const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
|
||||
const [email, setEmail] = useState(emailFromLink)
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
|
||||
const handleGetEMailVerificationCode = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@ import Link from 'next/link'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { emailRegex } from '@/config'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { webAppLogin } from '@/service/common'
|
||||
import { fetchAccessToken } from '@/service/share'
|
||||
|
|
@ -21,7 +20,7 @@ type MailAndPasswordAuthProps = {
|
|||
|
||||
export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAuthProps) {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { Mock } from 'vitest'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import I18nContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import CSVDownload from './csv-downloader'
|
||||
|
||||
|
|
@ -17,17 +18,13 @@ vi.mock('react-papaparse', () => ({
|
|||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: vi.fn(() => 'en-US'),
|
||||
}))
|
||||
|
||||
const renderWithLocale = (locale: Locale) => {
|
||||
return render(
|
||||
<I18nContext.Provider value={{
|
||||
locale,
|
||||
i18n: {},
|
||||
setLocaleOnClient: vi.fn().mockResolvedValue(undefined),
|
||||
}}
|
||||
>
|
||||
<CSVDownload />
|
||||
</I18nContext.Provider>,
|
||||
)
|
||||
;(useLocale as Mock).mockReturnValue(locale)
|
||||
return render(<CSVDownload />)
|
||||
}
|
||||
|
||||
describe('CSVDownload', () => {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next'
|
|||
import {
|
||||
useCSVDownloader,
|
||||
} from 'react-papaparse'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import I18n from '@/context/i18n'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
|
||||
const CSV_TEMPLATE_QA_EN = [
|
||||
|
|
@ -24,7 +24,7 @@ const CSV_TEMPLATE_QA_CN = [
|
|||
const CSVDownload: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const { CSVDownloader, Type } = useCSVDownloader()
|
||||
|
||||
const getTemplate = () => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import type { ComponentProps } from 'react'
|
||||
import type { Mock } from 'vitest'
|
||||
import type { AnnotationItemBasic } from '../type'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation'
|
||||
import HeaderOptions from './index'
|
||||
|
|
@ -163,12 +164,18 @@ vi.mock('@/app/components/billing/annotation-full', () => ({
|
|||
default: () => <div data-testid="annotation-full" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: vi.fn(() => LanguagesSupported[0]),
|
||||
}))
|
||||
|
||||
type HeaderOptionsProps = ComponentProps<typeof HeaderOptions>
|
||||
|
||||
const renderComponent = (
|
||||
props: Partial<HeaderOptionsProps> = {},
|
||||
locale: Locale = LanguagesSupported[0],
|
||||
) => {
|
||||
;(useLocale as Mock).mockReturnValue(locale)
|
||||
|
||||
const defaultProps: HeaderOptionsProps = {
|
||||
appId: 'test-app-id',
|
||||
onAdd: vi.fn(),
|
||||
|
|
@ -177,17 +184,7 @@ const renderComponent = (
|
|||
...props,
|
||||
}
|
||||
|
||||
return render(
|
||||
<I18NContext.Provider
|
||||
value={{
|
||||
locale,
|
||||
i18n: {},
|
||||
setLocaleOnClient: vi.fn(),
|
||||
}}
|
||||
>
|
||||
<HeaderOptions {...defaultProps} />
|
||||
</I18NContext.Provider>,
|
||||
)
|
||||
return render(<HeaderOptions {...defaultProps} />)
|
||||
}
|
||||
|
||||
const openOperationsPopover = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||
|
|
@ -440,20 +437,12 @@ describe('HeaderOptions', () => {
|
|||
await waitFor(() => expect(mockedFetchAnnotations).toHaveBeenCalledTimes(1))
|
||||
|
||||
view.rerender(
|
||||
<I18NContext.Provider
|
||||
value={{
|
||||
locale: LanguagesSupported[0],
|
||||
i18n: {},
|
||||
setLocaleOnClient: vi.fn(),
|
||||
}}
|
||||
>
|
||||
<HeaderOptions
|
||||
appId="test-app-id"
|
||||
onAdd={vi.fn()}
|
||||
onAdded={vi.fn()}
|
||||
controlUpdateList={1}
|
||||
/>
|
||||
</I18NContext.Provider>,
|
||||
<HeaderOptions
|
||||
appId="test-app-id"
|
||||
onAdd={vi.fn()}
|
||||
onAdded={vi.fn()}
|
||||
controlUpdateList={1}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(mockedFetchAnnotations).toHaveBeenCalledTimes(2))
|
||||
|
|
|
|||
|
|
@ -13,15 +13,14 @@ import { useTranslation } from 'react-i18next'
|
|||
import {
|
||||
useCSVDownloader,
|
||||
} from 'react-papaparse'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { FileDownload02, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import CustomPopover from '@/app/components/base/popover'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Button from '../../../base/button'
|
||||
import AddAnnotationModal from '../add-annotation-modal'
|
||||
import BatchAddModal from '../batch-add-annotation-modal'
|
||||
|
|
@ -44,7 +43,7 @@ const HeaderOptions: FC<Props> = ({
|
|||
controlUpdateList,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const { CSVDownloader, Type } = useCSVDownloader()
|
||||
const [list, setList] = useState<AnnotationItemBasic[]>([])
|
||||
const annotationUnavailable = list.length === 0
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { render, screen, waitFor } from '@testing-library/react'
|
|||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import I18n from '@/context/i18n'
|
||||
import SettingBuiltInTool from './setting-built-in-tool'
|
||||
|
||||
const fetchModelToolList = vi.fn()
|
||||
|
|
@ -56,6 +55,10 @@ vi.mock('@/app/components/plugins/readme-panel/entrance', () => ({
|
|||
ReadmeEntrance: ({ className }: { className?: string }) => <div className={className}>readme</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: vi.fn(() => 'en-US'),
|
||||
}))
|
||||
|
||||
const createParameter = (overrides?: Partial<ToolParameter>): ToolParameter => ({
|
||||
name: 'settingParam',
|
||||
label: {
|
||||
|
|
@ -129,18 +132,16 @@ const renderComponent = (props?: Partial<React.ComponentProps<typeof SettingBuil
|
|||
const onSave = vi.fn()
|
||||
const onAuthorizationItemClick = vi.fn()
|
||||
const utils = render(
|
||||
<I18n.Provider value={{ locale: 'en-US', i18n: {}, setLocaleOnClient: vi.fn() as any }}>
|
||||
<SettingBuiltInTool
|
||||
collection={baseCollection as any}
|
||||
toolName="search"
|
||||
isModel
|
||||
setting={{ settingParam: 'value' }}
|
||||
onHide={onHide}
|
||||
onSave={onSave}
|
||||
onAuthorizationItemClick={onAuthorizationItemClick}
|
||||
{...props}
|
||||
/>
|
||||
</I18n.Provider>,
|
||||
<SettingBuiltInTool
|
||||
collection={baseCollection as any}
|
||||
toolName="search"
|
||||
isModel
|
||||
setting={{ settingParam: 'value' }}
|
||||
onHide={onHide}
|
||||
onSave={onSave}
|
||||
onAuthorizationItemClick={onAuthorizationItemClick}
|
||||
{...props}
|
||||
/>,
|
||||
)
|
||||
return {
|
||||
...utils,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
|
|
@ -26,7 +25,7 @@ import {
|
|||
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n-config/language'
|
||||
import { fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList, fetchWorkflowToolList } from '@/service/tools'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
|
@ -58,7 +57,7 @@ const SettingBuiltInTool: FC<Props> = ({
|
|||
credentialId,
|
||||
onAuthorizationItemClick,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const language = getLanguage(locale)
|
||||
const { t } = useTranslation()
|
||||
const passedTools = (collection as ToolWithProvider).tools
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import type {
|
|||
import { noop } from 'es-toolkit/compat'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
import EmojiPicker from '@/app/components/base/emoji-picker'
|
||||
|
|
@ -16,7 +15,7 @@ import Modal from '@/app/components/base/modal'
|
|||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
||||
import I18n, { useDocLink } from '@/context/i18n'
|
||||
import { useDocLink, useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { useCodeBasedExtensions } from '@/service/use-common'
|
||||
|
||||
|
|
@ -41,7 +40,7 @@ const ExternalDataToolModal: FC<ExternalDataToolModalProps> = ({
|
|||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { notify } = useToastContext()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' })
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
const { data: codeBasedExtensionList } = useCodeBasedExtensions('external_data_tool')
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import {
|
|||
RiErrorWarningLine,
|
||||
} from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import I18n from '@/context/i18n'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -26,7 +26,7 @@ type Props = {
|
|||
|
||||
const ToolCallItem: FC<Props> = ({ toolCall, isLLM = false, isFinal, tokens, observation, finalAnswer }) => {
|
||||
const [collapseState, setCollapseState] = useState<boolean>(true)
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const toolName = isLLM ? 'LLM' : (toolCall.tool_label[locale] || toolCall.tool_label[locale.replaceAll('-', '_')])
|
||||
|
||||
const getTime = (time: number) => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import type { FC } from 'react'
|
||||
import type { CodeBasedExtensionForm } from '@/models/common'
|
||||
import type { ModerationConfig } from '@/models/debug'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
|
||||
type FormGenerationProps = {
|
||||
forms: CodeBasedExtensionForm[]
|
||||
|
|
@ -16,7 +15,7 @@ const FormGeneration: FC<FormGenerationProps> = ({
|
|||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
|
||||
const handleFormChange = (type: string, v: string) => {
|
||||
onChange({ ...value, [type]: v })
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import { produce } from 'immer'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
import { FeatureEnum } from '@/app/components/base/features/types'
|
||||
import { ContentModeration } from '@/app/components/base/icons/src/vender/features'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useCodeBasedExtensions } from '@/service/use-common'
|
||||
|
||||
|
|
@ -25,7 +23,7 @@ const Moderation = ({
|
|||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowModerationSettingModal } = useModalContext()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const moderation = useFeatures(s => s.features.moderation)
|
||||
const { data: codeBasedExtensionList } = useCodeBasedExtensions('moderation')
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { RiCloseLine } from '@remixicon/react'
|
|||
import { noop } from 'es-toolkit/compat'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
|
||||
|
|
@ -15,7 +14,7 @@ import { useToastContext } from '@/app/components/base/toast'
|
|||
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import I18n, { useDocLink } from '@/context/i18n'
|
||||
import { useDocLink, useLocale } from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { useCodeBasedExtensions, useModelProviders } from '@/service/use-common'
|
||||
|
|
@ -45,7 +44,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
|||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { notify } = useToastContext()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const { data: modelProviders, isPending: isLoading, refetch: refetchModelProviders } = useModelProviders()
|
||||
const [localeData, setLocaleData] = useState<ModerationConfig>(data)
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { useMemo } from 'react'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { usePipelineTemplateList } from '@/service/use-pipeline'
|
||||
import CreateCard from './create-card'
|
||||
import TemplateCard from './template-card'
|
||||
|
||||
const BuiltInPipelineList = () => {
|
||||
const { locale } = useI18N()
|
||||
const locale = useLocale()
|
||||
const language = useMemo(() => {
|
||||
if (['zh-Hans', 'ja-JP'].includes(locale))
|
||||
return locale
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import SimplePieChart from '@/app/components/base/simple-pie-chart'
|
|||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { upload } from '@/service/base'
|
||||
|
|
@ -40,7 +40,7 @@ const FileUploader = ({
|
|||
}: IFileUploaderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const dropRef = useRef<HTMLDivElement>(null)
|
||||
const dragRef = useRef<HTMLDivElement>(null)
|
||||
|
|
|
|||
|
|
@ -12,10 +12,8 @@ import {
|
|||
import { noop } from 'es-toolkit/compat'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
|
@ -38,7 +36,7 @@ import { useDefaultModel, useModelList, useModelListAndDefaultModelAndCurrentPro
|
|||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import { FULL_DOC_PREVIEW_LENGTH, IS_CE_EDITION } from '@/config'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import I18n, { useDocLink } from '@/context/i18n'
|
||||
import { useDocLink, useLocale } from '@/context/i18n'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { DataSourceProvider } from '@/models/common'
|
||||
|
|
@ -151,7 +149,7 @@ const StepTwo = ({
|
|||
}: StepTwoProps) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/u
|
|||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import DocumentFileIcon from '@/app/components/datasets/common/document-file-icon'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { upload } from '@/service/base'
|
||||
|
|
@ -33,7 +33,7 @@ const LocalFile = ({
|
|||
}: LocalFileProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const localFileList = useDataSourceStoreWithSelector(state => state.localFileList)
|
||||
const dataSourceStore = useDataSourceStore()
|
||||
const [dragging, setDragging] = useState(false)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@ import { useTranslation } from 'react-i18next'
|
|||
import {
|
||||
useCSVDownloader,
|
||||
} from 'react-papaparse'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
|
||||
|
|
@ -34,7 +33,7 @@ const CSV_TEMPLATE_CN = [
|
|||
|
||||
const CSVDownload: FC<{ docForm: ChunkingMode }> = ({ docForm }) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const { CSVDownloader, Type } = useCSVDownloader()
|
||||
|
||||
const getTemplate = () => {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
import { RiCloseLine, RiListUnordered } from '@remixicon/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { AppModeEnum, Theme } from '@/types/app'
|
||||
|
|
@ -26,7 +25,7 @@ type IDocProps = {
|
|||
}
|
||||
|
||||
const Doc = ({ appDetail }: IDocProps) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const { t } = useTranslation()
|
||||
const [toc, setToc] = useState<Array<{ href: string, text: string }>>([])
|
||||
const [isTocExpanded, setIsTocExpanded] = useState(false)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import { useContext } from 'use-context-selector'
|
|||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import I18n from '@/context/i18n'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
import { timezones } from '@/utils/timezone'
|
||||
|
|
@ -18,7 +20,7 @@ const titleClassName = `
|
|||
`
|
||||
|
||||
export default function LanguagePage() {
|
||||
const { locale, setLocaleOnClient } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const { userProfile, mutateUserProfile } = useAppContext()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [editing, setEditing] = useState(false)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import type { InvitationResult } from '@/models/common'
|
|||
import { RiPencilLine, RiUserAddLine } from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
|
@ -12,7 +11,7 @@ import { Plan } from '@/app/components/billing/type'
|
|||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
|
|
@ -34,7 +33,7 @@ const MembersPage = () => {
|
|||
dataset_operator: t('members.datasetOperator', { ns: 'common' }),
|
||||
normal: t('members.normal', { ns: 'common' }),
|
||||
}
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
|
||||
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
|
||||
const { data, refetch } = useMembers()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import Button from '@/app/components/base/button'
|
|||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { emailRegex } from '@/config'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import { inviteMember } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
|
@ -47,7 +47,7 @@ const InviteModal = ({
|
|||
setIsLimitExceeded(limited && (used > licenseLimit.workspace_members.limit))
|
||||
}, [licenseLimit, emails])
|
||||
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const [role, setRole] = useState<RoleKey>('normal')
|
||||
|
||||
const [isSubmitting, {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Mock } from 'vitest'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useLanguage } from './hooks'
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
|
|
@ -36,8 +36,7 @@ vi.mock('@/service/use-common', () => ({
|
|||
|
||||
// mock context hooks
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
__esModule: true,
|
||||
default: vi.fn(),
|
||||
useLocale: vi.fn(() => 'en-US'),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
|
|
@ -72,27 +71,20 @@ afterAll(() => {
|
|||
|
||||
describe('useLanguage', () => {
|
||||
it('should replace hyphen with underscore in locale', () => {
|
||||
(useContext as Mock).mockReturnValue({
|
||||
locale: 'en-US',
|
||||
})
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
const { result } = renderHook(() => useLanguage())
|
||||
expect(result.current).toBe('en_US')
|
||||
})
|
||||
|
||||
it('should return locale as is if no hyphen exists', () => {
|
||||
(useContext as Mock).mockReturnValue({
|
||||
locale: 'enUS',
|
||||
})
|
||||
;(useLocale as Mock).mockReturnValue('enUS')
|
||||
|
||||
const { result } = renderHook(() => useLanguage())
|
||||
expect(result.current).toBe('enUS')
|
||||
})
|
||||
|
||||
it('should handle multiple hyphens', () => {
|
||||
// Mock the I18n context return value
|
||||
(useContext as Mock).mockReturnValue({
|
||||
locale: 'zh-Hans-CN',
|
||||
})
|
||||
;(useLocale as Mock).mockReturnValue('zh-Hans-CN')
|
||||
|
||||
const { result } = renderHook(() => useLanguage())
|
||||
expect(result.current).toBe('zh_Hans-CN')
|
||||
|
|
|
|||
|
|
@ -16,14 +16,13 @@ import {
|
|||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import {
|
||||
useMarketplacePlugins,
|
||||
useMarketplacePluginsByCollectionId,
|
||||
} from '@/app/components/plugins/marketplace/hooks'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
|
|
@ -70,7 +69,7 @@ export const useSystemDefaultModelAndModelList: UseDefaultModelAndModelList = (
|
|||
}
|
||||
|
||||
export const useLanguage = () => {
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
return locale.replace('-', '_')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,20 +5,22 @@ import type { Locale } from '@/i18n-config'
|
|||
import { usePrefetchQuery } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
import { getSystemFeatures } from '@/service/common'
|
||||
import Loading from './base/loading'
|
||||
|
||||
import '../../i18n-config/i18next-config'
|
||||
|
||||
export type II18nProps = {
|
||||
locale: Locale
|
||||
children: React.ReactNode
|
||||
}
|
||||
const I18n: FC<II18nProps> = ({
|
||||
locale,
|
||||
children,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const locale = useLocale()
|
||||
|
||||
usePrefetchQuery({
|
||||
queryKey: ['systemFeatures'],
|
||||
|
|
@ -26,6 +28,7 @@ const I18n: FC<II18nProps> = ({
|
|||
})
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Setting locale on client:', locale)
|
||||
setLocaleOnClient(locale, false).then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
|
|
@ -35,14 +38,9 @@ const I18n: FC<II18nProps> = ({
|
|||
return <div className="flex h-screen w-screen items-center justify-center"><Loading type="app" /></div>
|
||||
|
||||
return (
|
||||
<I18NContext.Provider value={{
|
||||
locale,
|
||||
i18n: {},
|
||||
setLocaleOnClient,
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{children}
|
||||
</I18NContext.Provider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(I18n)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
/* eslint-disable dify-i18n/require-ns-option */
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import {
|
||||
getLocaleOnServer,
|
||||
getTranslation as translate,
|
||||
} from '@/i18n-config/server'
|
||||
import { getLocaleOnServer, getTranslation } from '@/i18n-config/server'
|
||||
|
||||
type DescriptionProps = {
|
||||
locale?: Locale
|
||||
|
|
@ -12,8 +9,8 @@ const Description = async ({
|
|||
locale: localeFromProps,
|
||||
}: DescriptionProps) => {
|
||||
const localeDefault = await getLocaleOnServer()
|
||||
const { t } = await translate(localeFromProps || localeDefault, 'plugin')
|
||||
const { t: tCommon } = await translate(localeFromProps || localeDefault, 'common')
|
||||
const { t } = await getTranslation(localeFromProps || localeDefault, 'plugin')
|
||||
const { t: tCommon } = await getTranslation(localeFromProps || localeDefault, 'common')
|
||||
const isZhHans = localeFromProps === 'zh-Hans'
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
|
|||
import { useTags } from '@/app/components/plugins/hooks'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { getPluginDetailLinkInMarketplace, getPluginLinkInMarketplace } from '../utils'
|
||||
|
||||
type CardWrapperProps = {
|
||||
|
|
@ -31,7 +31,7 @@ const CardWrapperComponent = ({
|
|||
setTrue: showInstallFromMarketplace,
|
||||
setFalse: hideInstallFromMarketplace,
|
||||
}] = useBoolean(false)
|
||||
const { locale: localeFromLocale } = useI18N()
|
||||
const localeFromLocale = useLocale()
|
||||
const { getTagLabel } = useTags(t)
|
||||
|
||||
// Memoize marketplace link params to prevent unnecessary re-renders
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-v
|
|||
import { API_PREFIX } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useGetLanguage, useI18N } from '@/context/i18n'
|
||||
import { useGetLanguage, useLocale } from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
|
|
@ -67,7 +67,7 @@ const DetailHeader = ({
|
|||
|
||||
const { theme } = useTheme()
|
||||
const locale = useGetLanguage()
|
||||
const { locale: currentLocale } = useI18N()
|
||||
const currentLocale = useLocale()
|
||||
const { checkForUpdates, fetchReleases } = useGitHubReleases()
|
||||
const { setShowUpdatePluginModal } = useModalContext()
|
||||
const { refreshModelProviders } = useProviderContext()
|
||||
|
|
|
|||
|
|
@ -6,11 +6,10 @@ import {
|
|||
} from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { getDocsUrl } from '@/app/components/plugins/utils'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useDebugKey } from '@/service/use-plugins'
|
||||
import KeyValueItem from '../base/key-value-item'
|
||||
|
||||
|
|
@ -18,7 +17,7 @@ const i18nPrefix = 'debugInfo'
|
|||
|
||||
const DebugInfo: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const { data: info, isLoading } = useDebugKey()
|
||||
|
||||
// info.key likes 4580bdb7-b878-471c-a8a4-bfd760263a53 mask the middle part using *.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { noop } from 'es-toolkit/compat'
|
|||
import Link from 'next/link'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import TabSlider from '@/app/components/base/tab-slider'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
|
@ -19,7 +18,7 @@ import ReferenceSettingModal from '@/app/components/plugins/reference-setting-mo
|
|||
import { getDocsUrl } from '@/app/components/plugins/utils'
|
||||
import { MARKETPLACE_API_PREFIX, SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { usePluginInstallation } from '@/hooks/use-query-params'
|
||||
import { fetchBundleInfoFromMarketPlace, fetchManifestFromMarketPlace } from '@/service/plugins'
|
||||
|
|
@ -48,7 +47,7 @@ const PluginPage = ({
|
|||
marketplace,
|
||||
}: PluginPageProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
useDocumentTitle(t('metadata.title', { ns: 'plugin' }))
|
||||
|
||||
// Use nuqs hook for installation state
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import Button from '@/app/components/base/button'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import { getPluginLinkInMarketplace } from '@/app/components/plugins/marketplace/utils'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Badge from '../base/badge'
|
||||
|
|
@ -36,7 +36,7 @@ const ProviderCardComponent: FC<Props> = ({
|
|||
setFalse: hideInstallFromMarketplace,
|
||||
}] = useBoolean(false)
|
||||
const { org, label } = payload
|
||||
const { locale } = useI18N()
|
||||
const locale = useLocale()
|
||||
|
||||
// Memoize the marketplace link params to prevent unnecessary re-renders
|
||||
const marketplaceLinkParams = useMemo(() => ({ language: locale, theme }), [locale, theme])
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import type { CustomCollectionBackend, CustomParamSchema } from '@/app/components/tools/types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { AuthType } from '@/app/components/tools/types'
|
||||
import I18n from '@/context/i18n'
|
||||
import { testAPIAvailable } from '@/service/tools'
|
||||
import TestApi from './test-api'
|
||||
|
||||
vi.mock('@/service/tools', () => ({
|
||||
testAPIAvailable: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: vi.fn(() => 'en-US'),
|
||||
}))
|
||||
|
||||
const testAPIAvailableMock = vi.mocked(testAPIAvailable)
|
||||
|
||||
describe('TestApi', () => {
|
||||
|
|
@ -40,19 +44,12 @@ describe('TestApi', () => {
|
|||
}
|
||||
|
||||
const renderTestApi = () => {
|
||||
const providerValue = {
|
||||
locale: 'en-US',
|
||||
i18n: {},
|
||||
setLocaleOnClient: vi.fn(),
|
||||
}
|
||||
return render(
|
||||
<I18n.Provider value={providerValue as any}>
|
||||
<TestApi
|
||||
customCollection={customCollection}
|
||||
tool={tool}
|
||||
onHide={vi.fn()}
|
||||
/>
|
||||
</I18n.Provider>,
|
||||
<TestApi
|
||||
customCollection={customCollection}
|
||||
tool={tool}
|
||||
onHide={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ import { RiSettings2Line } from '@remixicon/react'
|
|||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { AuthType } from '@/app/components/tools/types'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n-config/language'
|
||||
import { testAPIAvailable } from '@/service/tools'
|
||||
import ConfigCredentials from './config-credentials'
|
||||
|
|
@ -29,7 +28,7 @@ const TestApi: FC<Props> = ({
|
|||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const language = getLanguage(locale)
|
||||
const [credentialsModalShow, setCredentialsModalShow] = useState(false)
|
||||
const [tempCredential, setTempCredential] = React.useState<Credential>(customCollection.credentials)
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ import {
|
|||
} from '@remixicon/react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n-config/language'
|
||||
import { useCreateMCP } from '@/service/use-tools'
|
||||
import MCPModal from './modal'
|
||||
|
|
@ -20,7 +19,7 @@ type Props = {
|
|||
|
||||
const NewMCPCard = ({ handleCreate }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const language = getLanguage(locale)
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
import type { Tool } from '@/app/components/tools/types'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n-config/language'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
|
|
@ -15,7 +14,7 @@ type Props = {
|
|||
const MCPToolItem = ({
|
||||
tool,
|
||||
}: Props) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const language = getLanguage(locale)
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@ import {
|
|||
} from '@remixicon/react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import I18n, { useDocLink } from '@/context/i18n'
|
||||
import { useDocLink, useLocale } from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n-config/language'
|
||||
import { createCustomCollection } from '@/service/tools'
|
||||
|
||||
|
|
@ -21,7 +20,7 @@ type Props = {
|
|||
|
||||
const Contribute = ({ onRefreshData }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const language = getLanguage(locale)
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
|
|
@ -24,7 +23,7 @@ import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-m
|
|||
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
|
||||
import WorkflowToolModal from '@/app/components/tools/workflow-tool'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
|
|
@ -60,7 +59,7 @@ const ProviderDetail = ({
|
|||
onRefreshData,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const language = getLanguage(locale)
|
||||
|
||||
const needAuth = collection.allow_delete || collection.type === CollectionType.model
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
import type { Collection, Tool } from '../types'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import SettingBuiltInTool from '@/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n-config/language'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
|
|
@ -23,7 +22,7 @@ const ToolItem = ({
|
|||
isBuiltIn,
|
||||
isModel,
|
||||
}: Props) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const language = getLanguage(locale)
|
||||
const [showDetail, setShowDetail] = useState(false)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export type II18NHocProps = {
|
||||
children: ReactNode
|
||||
|
|
@ -10,7 +9,7 @@ export type II18NHocProps = {
|
|||
|
||||
const withI18N = (Component: any) => {
|
||||
return (props: any) => {
|
||||
const { i18n } = useContext(I18NContext)
|
||||
const { i18n } = useTranslation()
|
||||
return (
|
||||
<Component {...props} i18n={i18n} />
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import type { Plugin } from '@/app/components/plugins/types.ts'
|
|||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
|
@ -27,7 +26,7 @@ const Item: FC<Props> = ({
|
|||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const getLocalizedText = (obj: Record<string, string> | undefined) =>
|
||||
obj?.[locale] || obj?.['en-US'] || obj?.en_US || ''
|
||||
const [isShowInstallModal, {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ import type { Plugin } from '@/app/components/plugins/types'
|
|||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import { BlockEnum } from '../../types'
|
||||
|
||||
|
|
@ -17,7 +16,7 @@ const UninstalledItem = ({
|
|||
payload,
|
||||
}: UninstalledItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
|
||||
const getLocalizedText = (obj: Record<string, string> | undefined) =>
|
||||
obj?.[locale] || obj?.['en-US'] || obj?.en_US || ''
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ import type { DocExtractorNodeType } from './types'
|
|||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { useFileSupportTypes } from '@/service/use-common'
|
||||
import OutputVars, { VarItem } from '../_base/components/output-vars'
|
||||
|
|
@ -22,7 +21,7 @@ const Panel: FC<NodePanelProps<DocExtractorNodeType>> = ({
|
|||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const link = useNodeHelpLink(BlockEnum.DocExtractor)
|
||||
const { data: supportFileTypesResponse } = useFileSupportTypes()
|
||||
const supportTypes = supportFileTypesResponse?.allowed_extensions || []
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@ import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { sendResetPasswordCode, verifyResetPasswordCode } from '@/service/common'
|
||||
|
||||
export default function CheckCode() {
|
||||
|
|
@ -19,7 +18,7 @@ export default function CheckCode() {
|
|||
const token = decodeURIComponent(searchParams.get('token') as string)
|
||||
const [code, setVerifyCode] = useState('')
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
|
||||
const verify = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ import Link from 'next/link'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { emailRegex } from '@/config'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { sendResetPasswordCode } from '@/service/common'
|
||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '../components/signin/countdown'
|
||||
|
|
@ -22,7 +21,7 @@ export default function CheckCode() {
|
|||
const router = useRouter()
|
||||
const [email, setEmail] = useState('')
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
|
||||
const handleGetEMailVerificationCode = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
'use client'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import dynamic from 'next/dynamic'
|
||||
import * as React from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import LocaleSigninSelect from '@/app/components/base/select/locale-signin'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
|
||||
// Avoid rendering the logo and theme selector on the server
|
||||
|
|
@ -20,7 +19,7 @@ const ThemeSelector = dynamic(() => import('@/app/components/base/theme-selector
|
|||
})
|
||||
|
||||
const Header = () => {
|
||||
const { locale, setLocaleOnClient } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import I18NContext from '@/context/i18n'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
|
||||
import { encryptVerificationCode } from '@/utils/encryption'
|
||||
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
||||
|
|
@ -25,7 +25,7 @@ export default function CheckCode() {
|
|||
const language = i18n.language
|
||||
const [code, setVerifyCode] = useState('')
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
const codeInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const verify = async () => {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ import type { FormEvent } from 'react'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
|
||||
import { emailRegex } from '@/config'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { sendEMailLoginCode } from '@/service/common'
|
||||
|
||||
type MailAndCodeAuthProps = {
|
||||
|
|
@ -22,7 +21,7 @@ export default function MailAndCodeAuth({ isInvite }: MailAndCodeAuthProps) {
|
|||
const emailFromLink = decodeURIComponent(searchParams.get('email') || '')
|
||||
const [email, setEmail] = useState(emailFromLink)
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
|
||||
const handleGetEMailVerificationCode = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ import Link from 'next/link'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { emailRegex } from '@/config'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { login } from '@/service/common'
|
||||
import { encryptPassword } from '@/utils/encryption'
|
||||
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
||||
|
|
@ -23,7 +22,7 @@ type MailAndPasswordAuthProps = {
|
|||
|
||||
export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegistration: _allowRegistration }: MailAndPasswordAuthProps) {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ import Link from 'next/link'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import I18n, { useDocLink } from '@/context/i18n'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
import { languages, LanguagesSupported } from '@/i18n-config/language'
|
||||
import { activateMember } from '@/service/common'
|
||||
import { useInvitationCheck } from '@/service/use-common'
|
||||
|
|
@ -27,7 +27,6 @@ export default function InviteSettingsPage() {
|
|||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const token = decodeURIComponent(searchParams.get('invite_token') as string)
|
||||
const { setLocaleOnClient } = useContext(I18n)
|
||||
const [name, setName] = useState('')
|
||||
const [language, setLanguage] = useState(LanguagesSupported[0])
|
||||
const [timezone, setTimezone] = useState(() => Intl.DateTimeFormat().resolvedOptions().timeZone || 'America/Los_Angeles')
|
||||
|
|
@ -65,7 +64,7 @@ export default function InviteSettingsPage() {
|
|||
catch {
|
||||
recheck()
|
||||
}
|
||||
}, [language, name, recheck, setLocaleOnClient, timezone, token, router, t])
|
||||
}, [language, name, recheck, timezone, token, router, t])
|
||||
|
||||
if (!checkRes)
|
||||
return <Loading />
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@ import { RiArrowLeftLine, RiMailSendFill } from '@remixicon/react'
|
|||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useMailValidity, useSendMail } from '@/service/use-common'
|
||||
|
||||
export default function CheckCode() {
|
||||
|
|
@ -20,7 +19,7 @@ export default function CheckCode() {
|
|||
const [token, setToken] = useState(decodeURIComponent(searchParams.get('token') as string))
|
||||
const [code, setVerifyCode] = useState('')
|
||||
const [loading, setIsLoading] = useState(false)
|
||||
const { locale } = useContext(I18NContext)
|
||||
const locale = useLocale()
|
||||
const { mutateAsync: submitMail } = useSendMail()
|
||||
const { mutateAsync: verifyCode } = useMailValidity()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@ import { noop } from 'es-toolkit/compat'
|
|||
import Link from 'next/link'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Split from '@/app/signin/split'
|
||||
import { emailRegex } from '@/config'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useSendMail } from '@/service/use-common'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -22,7 +21,7 @@ export default function Form({
|
|||
}: Props) {
|
||||
const { t } = useTranslation()
|
||||
const [email, setEmail] = useState('')
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
|
||||
const { mutateAsync: submitMail, isPending } = useSendMail()
|
||||
|
|
|
|||
|
|
@ -1,33 +1,19 @@
|
|||
import type { Locale } from '@/i18n-config'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
} from 'use-context-selector'
|
||||
import type { Locale } from '@/i18n-config/language'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getDocLanguage, getLanguage, getPricingPageLanguage } from '@/i18n-config/language'
|
||||
|
||||
type II18NContext = {
|
||||
locale: Locale
|
||||
i18n: Record<string, any>
|
||||
setLocaleOnClient: (_lang: Locale, _reloadPage?: boolean) => Promise<void>
|
||||
export const useLocale = () => {
|
||||
const { i18n } = useTranslation()
|
||||
return i18n.language as Locale
|
||||
}
|
||||
|
||||
const I18NContext = createContext<II18NContext>({
|
||||
locale: 'en-US',
|
||||
i18n: {},
|
||||
setLocaleOnClient: async (_lang: Locale, _reloadPage?: boolean) => {
|
||||
noop()
|
||||
},
|
||||
})
|
||||
|
||||
export const useI18N = () => useContext(I18NContext)
|
||||
export const useGetLanguage = () => {
|
||||
const { locale } = useI18N()
|
||||
const locale = useLocale()
|
||||
|
||||
return getLanguage(locale)
|
||||
}
|
||||
export const useGetPricingPageLanguage = () => {
|
||||
const { locale } = useI18N()
|
||||
const locale = useLocale()
|
||||
|
||||
return getPricingPageLanguage(locale)
|
||||
}
|
||||
|
|
@ -36,7 +22,7 @@ export const defaultDocBaseUrl = 'https://docs.dify.ai'
|
|||
export const useDocLink = (baseUrl?: string): ((path?: string, pathMap?: { [index: string]: string }) => string) => {
|
||||
let baseDocUrl = baseUrl || defaultDocBaseUrl
|
||||
baseDocUrl = (baseDocUrl.endsWith('/')) ? baseDocUrl.slice(0, -1) : baseDocUrl
|
||||
const { locale } = useI18N()
|
||||
const locale = useLocale()
|
||||
const docLanguage = getDocLanguage(locale)
|
||||
return (path?: string, pathMap?: { [index: string]: string }): string => {
|
||||
const pathUrl = path || ''
|
||||
|
|
@ -45,4 +31,3 @@ export const useDocLink = (baseUrl?: string): ((path?: string, pathMap?: { [inde
|
|||
return `${baseDocUrl}/${docLanguage}/${targetPath}`
|
||||
}
|
||||
}
|
||||
export default I18NContext
|
||||
|
|
|
|||
|
|
@ -14,15 +14,13 @@ import type { Mock } from 'vitest'
|
|||
*/
|
||||
import { renderHook } from '@testing-library/react'
|
||||
// Import after mock to get the mocked version
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
|
||||
import { useFormatTimeFromNow } from './use-format-time-from-now'
|
||||
|
||||
// Mock the i18n context
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useI18N: vi.fn(() => ({
|
||||
locale: 'en-US',
|
||||
})),
|
||||
useLocale: vi.fn(() => 'en-US'),
|
||||
}))
|
||||
|
||||
describe('useFormatTimeFromNow', () => {
|
||||
|
|
@ -47,7 +45,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* Should return human-readable relative time strings
|
||||
*/
|
||||
it('should format time from now in English', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'en-US' })
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -65,7 +63,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* Very recent timestamps should show seconds
|
||||
*/
|
||||
it('should format very recent times', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'en-US' })
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -81,7 +79,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* Should handle day-level granularity
|
||||
*/
|
||||
it('should format times from days ago', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'en-US' })
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -98,7 +96,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* dayjs fromNow also supports future times (e.g., "in 2 hours")
|
||||
*/
|
||||
it('should format future times', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'en-US' })
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -117,7 +115,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* Should use Chinese characters for time units
|
||||
*/
|
||||
it('should format time in Chinese (Simplified)', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'zh-Hans' })
|
||||
;(useLocale as Mock).mockReturnValue('zh-Hans')
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -134,7 +132,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* Should use Spanish words for relative time
|
||||
*/
|
||||
it('should format time in Spanish', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'es-ES' })
|
||||
;(useLocale as Mock).mockReturnValue('es-ES')
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -151,7 +149,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* Should use French words for relative time
|
||||
*/
|
||||
it('should format time in French', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'fr-FR' })
|
||||
;(useLocale as Mock).mockReturnValue('fr-FR')
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -168,7 +166,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* Should use Japanese characters
|
||||
*/
|
||||
it('should format time in Japanese', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'ja-JP' })
|
||||
;(useLocale as Mock).mockReturnValue('ja-JP')
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -185,7 +183,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* Should use pt-br locale mapping
|
||||
*/
|
||||
it('should format time in Portuguese (Brazil)', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'pt-BR' })
|
||||
;(useLocale as Mock).mockReturnValue('pt-BR')
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -202,7 +200,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* Unknown locales should default to English
|
||||
*/
|
||||
it('should fallback to English for unsupported locale', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'xx-XX' as any })
|
||||
;(useLocale as Mock).mockReturnValue('xx-XX' as any)
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -222,7 +220,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* Should format as a very old date
|
||||
*/
|
||||
it('should handle timestamp 0', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'en-US' })
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -238,7 +236,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* Should handle dates far in the future
|
||||
*/
|
||||
it('should handle very large timestamps', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'en-US' })
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -260,12 +258,12 @@ describe('useFormatTimeFromNow', () => {
|
|||
const oneHourAgo = now - (60 * 60 * 1000)
|
||||
|
||||
// First render with English
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'en-US' })
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
rerender()
|
||||
const englishResult = result.current.formatTimeFromNow(oneHourAgo)
|
||||
|
||||
// Second render with Spanish
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'es-ES' })
|
||||
;(useLocale as Mock).mockReturnValue('es-ES')
|
||||
rerender()
|
||||
const spanishResult = result.current.formatTimeFromNow(oneHourAgo)
|
||||
|
||||
|
|
@ -280,7 +278,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* dayjs should automatically choose the appropriate unit
|
||||
*/
|
||||
it('should use appropriate time units for different durations', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'en-US' })
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -342,7 +340,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
const oneHourAgo = now - (60 * 60 * 1000)
|
||||
|
||||
locales.forEach((locale) => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale })
|
||||
;(useLocale as Mock).mockReturnValue(locale)
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
const formatted = result.current.formatTimeFromNow(oneHourAgo)
|
||||
|
|
@ -360,7 +358,7 @@ describe('useFormatTimeFromNow', () => {
|
|||
* The formatTimeFromNow function should be memoized with useCallback
|
||||
*/
|
||||
it('should memoize formatTimeFromNow function', () => {
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'en-US' })
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
|
||||
const { result, rerender } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
|
|
@ -379,11 +377,11 @@ describe('useFormatTimeFromNow', () => {
|
|||
it('should create new function when locale changes', () => {
|
||||
const { result, rerender } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'en-US' })
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
rerender()
|
||||
const englishFunction = result.current.formatTimeFromNow
|
||||
|
||||
;(useI18N as Mock).mockReturnValue({ locale: 'es-ES' })
|
||||
;(useLocale as Mock).mockReturnValue('es-ES')
|
||||
rerender()
|
||||
const spanishFunction = result.current.formatTimeFromNow
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useCallback } from 'react'
|
||||
import { useI18N } from '@/context/i18n'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { localeMap } from '@/i18n-config/language'
|
||||
import 'dayjs/locale/de'
|
||||
import 'dayjs/locale/es'
|
||||
|
|
@ -27,7 +27,7 @@ import 'dayjs/locale/zh-tw'
|
|||
dayjs.extend(relativeTime)
|
||||
|
||||
export const useFormatTimeFromNow = () => {
|
||||
const { locale } = useI18N()
|
||||
const locale = useLocale()
|
||||
const formatTimeFromNow = useCallback((time: number) => {
|
||||
const dayjsLocale = localeMap[locale] ?? 'en'
|
||||
return dayjs(time).locale(dayjsLocale).fromNow()
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
- useTranslation
|
||||
- useGetLanguage
|
||||
- useI18N
|
||||
- useLocale
|
||||
- useRenderI18nObject
|
||||
|
||||
## impl
|
||||
|
|
@ -46,6 +46,6 @@
|
|||
## TODO
|
||||
|
||||
- [ ] ts docs for useGetLanguage
|
||||
- [ ] ts docs for useI18N
|
||||
- [ ] ts docs for useLocale
|
||||
- [ ] client docs for i18n
|
||||
- [ ] server docs for i18n
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type { Locale } from '.'
|
||||
import { camelCase, kebabCase } from 'es-toolkit/compat'
|
||||
import i18n from 'i18next'
|
||||
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import appAnnotation from '../i18n/en-US/app-annotation.json'
|
||||
import appApi from '../i18n/en-US/app-api.json'
|
||||
|
|
@ -122,7 +122,7 @@ const getInitialTranslations = () => {
|
|||
}
|
||||
|
||||
if (!i18n.isInitialized) {
|
||||
i18n.use(initReactI18next).init({
|
||||
i18n.use(LanguageDetector).use(initReactI18next).init({
|
||||
lng: undefined,
|
||||
fallbackLng: 'en-US',
|
||||
resources: getInitialTranslations(),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { i18n as I18nInstance } from 'i18next'
|
||||
import type { Locale } from '.'
|
||||
import type { NamespaceCamelCase, NamespaceKebabCase } from './i18next-config'
|
||||
import { match } from '@formatjs/intl-localematcher'
|
||||
|
|
@ -7,29 +8,39 @@ import resourcesToBackend from 'i18next-resources-to-backend'
|
|||
import Negotiator from 'negotiator'
|
||||
import { cookies, headers } from 'next/headers'
|
||||
import { initReactI18next } from 'react-i18next/initReactI18next'
|
||||
import serverOnlyContext from '@/utils/server-only-context'
|
||||
import { i18n } from '.'
|
||||
|
||||
// https://locize.com/blog/next-13-app-dir-i18n/
|
||||
const initI18next = async (lng: Locale, ns: NamespaceKebabCase) => {
|
||||
const i18nInstance = createInstance()
|
||||
await i18nInstance
|
||||
const [getLocaleCache, setLocaleCache] = serverOnlyContext<Locale | null>(null)
|
||||
const [getI18nInstance, setI18nInstance] = serverOnlyContext<I18nInstance | null>(null)
|
||||
|
||||
const getOrCreateI18next = async (lng: Locale) => {
|
||||
let instance = getI18nInstance()
|
||||
if (instance)
|
||||
return instance
|
||||
|
||||
instance = createInstance()
|
||||
await instance
|
||||
.use(initReactI18next)
|
||||
.use(resourcesToBackend((language: Locale, namespace: NamespaceKebabCase) => {
|
||||
return import(`../i18n/${language}/${namespace}.json`)
|
||||
}))
|
||||
.init({
|
||||
lng: lng === 'zh-Hans' ? 'zh-Hans' : lng,
|
||||
ns,
|
||||
defaultNS: ns,
|
||||
lng,
|
||||
fallbackLng: 'en-US',
|
||||
keySeparator: false,
|
||||
})
|
||||
return i18nInstance
|
||||
setI18nInstance(instance)
|
||||
return instance
|
||||
}
|
||||
|
||||
export async function getTranslation(lng: Locale, ns: NamespaceKebabCase) {
|
||||
const camelNs = camelCase(ns) as NamespaceCamelCase
|
||||
const i18nextInstance = await initI18next(lng, ns)
|
||||
const i18nextInstance = await getOrCreateI18next(lng)
|
||||
|
||||
if (!i18nextInstance.hasLoadedNamespace(camelNs))
|
||||
await i18nextInstance.loadNamespaces(camelNs)
|
||||
|
||||
return {
|
||||
t: i18nextInstance.getFixedT(lng, camelNs),
|
||||
i18n: i18nextInstance,
|
||||
|
|
@ -37,6 +48,10 @@ export async function getTranslation(lng: Locale, ns: NamespaceKebabCase) {
|
|||
}
|
||||
|
||||
export const getLocaleOnServer = async (): Promise<Locale> => {
|
||||
const cached = getLocaleCache()
|
||||
if (cached)
|
||||
return cached
|
||||
|
||||
const locales: string[] = i18n.locales
|
||||
|
||||
let languages: string[] | undefined
|
||||
|
|
@ -58,5 +73,6 @@ export const getLocaleOnServer = async (): Promise<Locale> => {
|
|||
|
||||
// match locale
|
||||
const matchedLocale = match(languages, locales, i18n.defaultLocale) as Locale
|
||||
setLocaleCache(matchedLocale)
|
||||
return matchedLocale
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@
|
|||
"html-entities": "^2.6.0",
|
||||
"html-to-image": "1.11.13",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"immer": "^11.1.0",
|
||||
"js-audio-recorder": "^1.0.7",
|
||||
|
|
|
|||
|
|
@ -186,6 +186,9 @@ importers:
|
|||
i18next:
|
||||
specifier: ^23.16.8
|
||||
version: 23.16.8
|
||||
i18next-browser-languagedetector:
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0
|
||||
i18next-resources-to-backend:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
|
|
@ -5950,6 +5953,9 @@ packages:
|
|||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
i18next-browser-languagedetector@8.2.0:
|
||||
resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==}
|
||||
|
||||
i18next-resources-to-backend@1.2.1:
|
||||
resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==}
|
||||
|
||||
|
|
@ -15120,6 +15126,10 @@ snapshots:
|
|||
|
||||
husky@9.1.7: {}
|
||||
|
||||
i18next-browser-languagedetector@8.2.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
|
||||
i18next-resources-to-backend@1.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
// credit: https://github.com/manvalls/server-only-context/blob/main/src/index.ts
|
||||
|
||||
import { cache } from 'react'
|
||||
|
||||
export default <T>(defaultValue: T): [() => T, (v: T) => void] => {
|
||||
const getRef = cache(() => ({ current: defaultValue }))
|
||||
|
||||
const getValue = (): T => getRef().current
|
||||
|
||||
const setValue = (value: T) => {
|
||||
getRef().current = value
|
||||
}
|
||||
|
||||
return [getValue, setValue]
|
||||
}
|
||||
Loading…
Reference in New Issue