refactor(i18n): about locales (#30336)

Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Stephen Zhou 2025-12-30 14:38:23 +08:00 committed by GitHub
parent 3505516e8e
commit 2399d00d86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 273 additions and 320 deletions

View File

@ -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 (

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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 {

View File

@ -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)

View File

@ -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', () => {

View File

@ -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 = () => {

View File

@ -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))

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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')

View File

@ -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) => {

View File

@ -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 })

View File

@ -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')

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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 = () => {

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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, {

View File

@ -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')

View File

@ -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('-', '_')
}

View File

@ -3,9 +3,10 @@
import type { FC } from 'react'
import type { Locale } from '@/i18n-config'
import { usePrefetchQuery } from '@tanstack/react-query'
import { useHydrateAtoms } from 'jotai/utils'
import * as React from 'react'
import { useEffect, useState } from 'react'
import I18NContext from '@/context/i18n'
import { localeAtom } from '@/context/i18n'
import { setLocaleOnClient } from '@/i18n-config'
import { getSystemFeatures } from '@/service/common'
import Loading from './base/loading'
@ -18,6 +19,7 @@ const I18n: FC<II18nProps> = ({
locale,
children,
}) => {
useHydrateAtoms([[localeAtom, locale]])
const [loading, setLoading] = useState(true)
usePrefetchQuery({
@ -35,14 +37,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)

View File

@ -46,7 +46,6 @@ vi.mock('../marketplace/hooks', () => ({
// Mock useGetLanguage context
vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en-US',
useI18N: () => ({ locale: 'en-US' }),
}))
// Mock useTheme hook

View File

@ -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 (

View File

@ -191,11 +191,9 @@ vi.mock('next-themes', () => ({
}),
}))
// Mock useI18N context
// Mock useLocale context
vi.mock('@/context/i18n', () => ({
useI18N: () => ({
locale: 'en-US',
}),
useLocale: () => 'en-US',
}))
// Mock i18n-config/language

View File

@ -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

View File

@ -49,11 +49,9 @@ vi.mock('../context', () => ({
useMarketplaceContext: (selector: (v: typeof mockContextValues) => unknown) => selector(mockContextValues),
}))
// Mock useI18N context
// Mock useLocale context
vi.mock('@/context/i18n', () => ({
useI18N: () => ({
locale: 'en-US',
}),
useLocale: () => 'en-US',
}))
// Mock next-themes

View File

@ -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()

View File

@ -29,7 +29,6 @@ vi.mock('../marketplace/hooks', () => ({
// Mock useGetLanguage context
vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en-US',
useI18N: () => ({ locale: 'en-US' }),
}))
// Mock useTheme hook

View File

@ -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 *.

View File

@ -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

View File

@ -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])

View File

@ -51,7 +51,6 @@ vi.mock('react-i18next', async (importOriginal) => {
// Mock useGetLanguage context
vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en-US',
useI18N: () => ({ locale: 'en-US' }),
}))
// Mock app context for useGetIcon

View File

@ -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()}
/>,
)
}

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -1,20 +0,0 @@
'use client'
import type { ReactNode } from 'react'
import { useContext } from 'use-context-selector'
import I18NContext from '@/context/i18n'
export type II18NHocProps = {
children: ReactNode
}
const withI18N = (Component: any) => {
return (props: any) => {
const { i18n } = useContext(I18NContext)
return (
<Component {...props} i18n={i18n} />
)
}
}
export default withI18N

View File

@ -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, {

View File

@ -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 || ''

View File

@ -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 || []

View File

@ -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 {

View File

@ -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 {

View File

@ -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 (

View File

@ -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 () => {

View File

@ -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 {

View File

@ -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)

View File

@ -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 />

View File

@ -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()

View File

@ -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()

View File

@ -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 { atom, useAtomValue } from 'jotai'
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 localeAtom = atom<Locale>('en-US')
export const useLocale = () => {
return useAtomValue(localeAtom)
}
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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -2,7 +2,6 @@
import type { Locale } from '.'
import { camelCase, kebabCase } from 'es-toolkit/compat'
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import appAnnotation from '../i18n/en-US/app-annotation.json'
import appApi from '../i18n/en-US/app-api.json'

View File

@ -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
}

View File

@ -92,6 +92,7 @@
"i18next": "^25.7.3",
"i18next-resources-to-backend": "^1.2.1",
"immer": "^11.1.0",
"jotai": "^2.16.1",
"js-audio-recorder": "^1.0.7",
"js-cookie": "^3.0.5",
"js-yaml": "^4.1.0",

View File

@ -192,6 +192,9 @@ importers:
immer:
specifier: ^11.1.0
version: 11.1.0
jotai:
specifier: ^2.16.1
version: 2.16.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.7)(react@19.2.3)
js-audio-recorder:
specifier: ^1.0.7
version: 1.0.7
@ -6207,6 +6210,24 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
jotai@2.16.1:
resolution: {integrity: sha512-vrHcAbo3P7Br37C8Bv6JshMtlKMPqqmx0DDREtTjT4nf3QChDrYdbH+4ik/9V0cXA57dK28RkJ5dctYvavcIlg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@babel/core': '>=7.0.0'
'@babel/template': '>=7.0.0'
'@types/react': ~19.2.7
react: '>=17.0.0'
peerDependenciesMeta:
'@babel/core':
optional: true
'@babel/template':
optional: true
'@types/react':
optional: true
react:
optional: true
js-audio-recorder@1.0.7:
resolution: {integrity: sha512-JiDODCElVHGrFyjGYwYyNi7zCbKk9va9C77w+zCPMmi4C6ix7zsX2h3ddHugmo4dOTOTCym9++b/wVW9nC0IaA==}
@ -15331,6 +15352,13 @@ snapshots:
jiti@2.6.1: {}
jotai@2.16.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.7)(react@19.2.3):
optionalDependencies:
'@babel/core': 7.28.5
'@babel/template': 7.27.2
'@types/react': 19.2.7
react: 19.2.3
js-audio-recorder@1.0.7: {}
js-base64@3.7.8: {}

View File

@ -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]
}