perf(web): improve vinext home startup time (#38219)

This commit is contained in:
Stephen Zhou 2026-06-30 18:36:38 +08:00 committed by GitHub
parent 200f8b800f
commit b1bb6ef977
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 178 additions and 92 deletions

View File

@ -0,0 +1,21 @@
'use client'
import dynamic from '@/next/dynamic'
const InSiteMessageNotification = dynamic(() => import('@/app/components/app/in-site-message/notification'), { ssr: false })
const PartnerStack = dynamic(() => import('@/app/components/billing/partner-stack'), { ssr: false })
const ReadmePanel = dynamic(() => import('@/app/components/plugins/readme-panel'), { ssr: false })
const WorkflowGeneratorMount = dynamic(() => import('@/app/components/workflow/workflow-generator/mount'), { ssr: false })
const GotoAnything = dynamic(() => import('@/app/components/goto-anything').then(mod => mod.GotoAnything), { ssr: false })
export function CommonLayoutGlobalMounts() {
return (
<>
<InSiteMessageNotification />
<PartnerStack />
<ReadmePanel />
<GotoAnything />
<WorkflowGeneratorMount />
</>
)
}

View File

@ -1,20 +1,16 @@
import type { ReactNode } from 'react'
import InSiteMessageNotification from '@/app/components/app/in-site-message/notification'
import AmplitudeProvider from '@/app/components/base/amplitude'
import { GoogleAnalyticsScripts } from '@/app/components/base/ga'
import Zendesk from '@/app/components/base/zendesk'
import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder'
import { GotoAnything } from '@/app/components/goto-anything'
import MainNavLayout from '@/app/components/main-nav/layout'
import { NextRouteStateBridge } from '@/app/components/next-route-state'
import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics'
import ReadmePanel from '@/app/components/plugins/readme-panel'
import WorkflowGeneratorMount from '@/app/components/workflow/workflow-generator/mount'
import { AppContextProvider } from '@/context/app-context-provider'
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
import { ModalContextProvider } from '@/context/modal-context-provider'
import { ProviderContextProvider } from '@/context/provider-context-provider'
import PartnerStack from '../components/billing/partner-stack'
import { CommonLayoutGlobalMounts } from './global-mounts'
import { CommonLayoutHydrationBoundary } from './hydration-boundary'
export default async function Layout({ children }: { children: ReactNode }) {
@ -33,11 +29,7 @@ export default async function Layout({ children }: { children: ReactNode }) {
<MainNavLayout>
{children}
</MainNavLayout>
<InSiteMessageNotification />
<PartnerStack />
<ReadmePanel />
<GotoAnything />
<WorkflowGeneratorMount />
<CommonLayoutGlobalMounts />
</ModalContextProvider>
</ProviderContextProvider>
</EventEmitterContextProvider>

View File

@ -495,7 +495,7 @@ describe('MainNav', () => {
expect(screen.queryByRole('link', { name: /common.menus.deployments/ })).not.toBeInTheDocument()
})
it('aligns the global navigation spacing with the main sidebar design', () => {
it('aligns the global navigation spacing with the main sidebar design', async () => {
mockInstalledApps = [createInstalledApp()]
renderMainNav()
@ -508,7 +508,7 @@ describe('MainNav', () => {
expect(homeLink.closest('nav')).toHaveClass('isolate', 'flex', 'flex-col', 'gap-px', 'p-2')
expect(homeLink).toHaveClass('h-8', 'w-full', 'rounded-[10px]', 'px-2', 'py-1.5')
const webAppsButton = screen.getByRole('button', { name: 'explore.sidebar.webApps' })
const webAppsButton = await screen.findByRole('button', { name: 'explore.sidebar.webApps' })
expect(webAppsButton.parentElement).toHaveClass('py-1', 'pr-2', 'pl-2')
const helpButton = screen.getByRole('button', { name: 'common.mainNav.help.openMenu' })
@ -551,7 +551,7 @@ describe('MainNav', () => {
expect(container.querySelector('.relative.z-30')).not.toBeInTheDocument()
})
it('hides the environment tag when app detail navigation is collapsed', () => {
it('hides the environment tag when app detail navigation is collapsed', async () => {
mockPathname = '/app/app-1/overview'
;(useAppContext as Mock).mockReturnValue({
...appContextValue,
@ -562,7 +562,7 @@ describe('MainNav', () => {
})
const { container } = renderMainNav()
fireEvent.click(screen.getByTestId('app-detail-toggle'))
fireEvent.click(await screen.findByTestId('app-detail-toggle'))
expect(screen.queryByText('common.environment.testing')).not.toBeInTheDocument()
expect(container.querySelector('.relative.z-30')).not.toBeInTheDocument()
@ -659,7 +659,7 @@ describe('MainNav', () => {
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).not.toHaveAttribute('aria-current')
})
it('replaces global navigation with snippet detail navigation on snippet routes', () => {
it('replaces global navigation with snippet detail navigation on snippet routes', async () => {
mockPathname = '/snippets/snippet-1/orchestrate'
snippetDraftState.inputFields = snippetFields
snippetNavigationState.onFieldsChange = mockSnippetFieldsChange
@ -671,8 +671,8 @@ describe('MainNav', () => {
expect(screen.getByRole('complementary')).toHaveClass('w-62')
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByRole('complementary')).toHaveClass('bg-background-body')
expect(screen.getByTestId('snippet-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('snippet-sidebar-content')).toHaveAttribute('data-readonly', 'false')
expect(await screen.findByTestId('snippet-detail-top')).toHaveAttribute('data-expand', 'true')
expect(await screen.findByTestId('snippet-sidebar-content')).toHaveAttribute('data-readonly', 'false')
expect(screen.getByText(snippet.name)).toBeInTheDocument()
expect(screen.getByText('query')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'change snippet fields' }))
@ -686,14 +686,14 @@ describe('MainNav', () => {
expect(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' })).toBeInTheDocument()
})
it('collapses snippet detail navigation from the top-right toggle', () => {
it('collapses snippet detail navigation from the top-right toggle', async () => {
mockPathname = '/snippets/snippet-1/orchestrate'
snippetDraftState.inputFields = snippetFields
snippetNavigationState.onFieldsChange = mockSnippetFieldsChange
snippetNavigationState.snippet = snippet
renderMainNav()
fireEvent.click(screen.getByTestId('snippet-detail-toggle'))
fireEvent.click(await screen.findByTestId('snippet-detail-toggle'))
expect(screen.getByRole('complementary')).toHaveClass('w-16')
expect(screen.getByRole('complementary')).toHaveClass('p-1')
@ -704,13 +704,13 @@ describe('MainNav', () => {
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
})
it('replaces global navigation with app detail navigation on app routes', () => {
it('replaces global navigation with app detail navigation on app routes', async () => {
mockPathname = '/app/app-1/overview'
renderMainNav()
expect(screen.getByTestId('app-detail-top')).toBeInTheDocument()
expect(screen.getByTestId('app-detail-section')).toBeInTheDocument()
expect(await screen.findByTestId('app-detail-top')).toBeInTheDocument()
expect(await screen.findByTestId('app-detail-section')).toBeInTheDocument()
expect(screen.getByTestId('app-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('app-detail-section')).toHaveAttribute('data-expand', 'true')
expect(screen.getByRole('complementary')).toHaveClass('w-62')
@ -742,56 +742,56 @@ describe('MainNav', () => {
})
})
it('collapses app detail navigation from the top-right toggle', () => {
it('collapses app detail navigation from the top-right toggle', async () => {
mockPathname = '/app/app-1/overview'
renderMainNav()
fireEvent.click(screen.getByTestId('app-detail-toggle'))
fireEvent.click(await screen.findByTestId('app-detail-toggle'))
expect(screen.getByRole('complementary')).toHaveClass('w-16')
expect(screen.getByRole('complementary')).not.toHaveClass('transition-none')
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByTestId('app-detail-top')).toHaveAttribute('data-expand', 'false')
expect(screen.getByTestId('app-detail-section')).toHaveAttribute('data-expand', 'false')
expect(await screen.findByTestId('app-detail-section')).toHaveAttribute('data-expand', 'false')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
})
it('shows app detail navigation as a floating preview when hovering the collapsed top toggle', () => {
it('shows app detail navigation as a floating preview when hovering the collapsed top toggle', async () => {
mockPathname = '/app/app-1/overview'
renderMainNav()
fireEvent.click(screen.getByTestId('app-detail-toggle'))
fireEvent.click(await screen.findByTestId('app-detail-toggle'))
fireEvent.mouseEnter(screen.getByTestId('app-detail-top').parentElement!)
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
expect(screen.getAllByTestId('app-detail-top')).toHaveLength(1)
expect(screen.getByTestId('app-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('app-detail-section')).toHaveAttribute('data-expand', 'true')
expect(await screen.findByTestId('app-detail-section')).toHaveAttribute('data-expand', 'true')
})
it('persists expanded app detail navigation without width animation when clicking the hovered toggle', () => {
it('persists expanded app detail navigation without width animation when clicking the hovered toggle', async () => {
mockPathname = '/app/app-1/overview'
renderMainNav()
fireEvent.click(screen.getByTestId('app-detail-toggle'))
fireEvent.click(await screen.findByTestId('app-detail-toggle'))
fireEvent.mouseEnter(screen.getByTestId('app-detail-top').parentElement!)
fireEvent.click(screen.getByTestId('app-detail-toggle'))
expect(screen.getByRole('complementary')).toHaveClass('w-62', 'transition-none')
expect(screen.getByRole('complementary')).not.toHaveClass('overflow-visible')
expect(screen.getByTestId('app-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('app-detail-section')).toHaveAttribute('data-expand', 'true')
expect(await screen.findByTestId('app-detail-section')).toHaveAttribute('data-expand', 'true')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('expand')
})
it('replaces global navigation with dataset detail navigation on dataset routes', () => {
it('replaces global navigation with dataset detail navigation on dataset routes', async () => {
mockPathname = '/datasets/dataset-1/documents'
renderMainNav()
expect(screen.getByTestId('dataset-detail-top')).toBeInTheDocument()
expect(screen.getByTestId('dataset-detail-section')).toBeInTheDocument()
expect(await screen.findByTestId('dataset-detail-top')).toBeInTheDocument()
expect(await screen.findByTestId('dataset-detail-section')).toBeInTheDocument()
expect(screen.getByTestId('dataset-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'true')
expect(screen.getByRole('complementary')).toHaveClass('w-62')
@ -802,42 +802,44 @@ describe('MainNav', () => {
expect(screen.queryByRole('link', { name: /common.menus.datasets/ })).not.toBeInTheDocument()
})
it('collapses dataset detail navigation from the top-right toggle', () => {
it('collapses dataset detail navigation from the top-right toggle', async () => {
mockPathname = '/datasets/dataset-1/documents'
renderMainNav()
fireEvent.click(screen.getByTestId('dataset-detail-toggle'))
fireEvent.click(await screen.findByTestId('dataset-detail-toggle'))
expect(screen.getByRole('complementary')).toHaveClass('w-16')
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByTestId('dataset-detail-top')).toHaveAttribute('data-expand', 'false')
expect(screen.getByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'false')
expect(await screen.findByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'false')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
})
it('shows dataset detail navigation as a floating preview when hovering the collapsed top toggle', () => {
it('shows dataset detail navigation as a floating preview when hovering the collapsed top toggle', async () => {
mockPathname = '/datasets/dataset-1/documents'
renderMainNav()
fireEvent.click(screen.getByTestId('dataset-detail-toggle'))
fireEvent.click(await screen.findByTestId('dataset-detail-toggle'))
fireEvent.mouseEnter(screen.getByTestId('dataset-detail-top').parentElement!)
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
expect(screen.getAllByTestId('dataset-detail-top')).toHaveLength(1)
expect(screen.getByTestId('dataset-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'true')
expect(await screen.findByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'true')
})
it('replaces global navigation with agent detail navigation on roster detail routes', () => {
it('replaces global navigation with agent detail navigation on roster detail routes', async () => {
mockPathname = '/roster/agent/agent-1/configure'
renderMainNav()
expect(screen.getByTestId('agent-detail-top')).toBeInTheDocument()
expect(screen.getByTestId('agent-detail-section')).toBeInTheDocument()
expect(await screen.findByTestId('agent-detail-top')).toBeInTheDocument()
const agentDetailNavigation = await screen.findByRole('navigation', { name: 'agentV2.agentDetail.navigationLabel' })
expect(agentDetailNavigation).toBeInTheDocument()
expect(screen.getByRole('link', { name: /agentV2.agentDetail.sections.configure/ })).toHaveAttribute('href', '/roster/agent/agent-1/configure')
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')
expect(agentDetailNavigation).toHaveClass('px-1')
expect(screen.getByRole('complementary')).toHaveClass('w-62')
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByRole('complementary')).toHaveClass('bg-background-body')
@ -873,16 +875,16 @@ describe('MainNav', () => {
expect(screen.queryByRole('link', { name: /common.menus.deployments/ })).not.toBeInTheDocument()
})
it('collapses agent detail navigation from the top-right toggle', () => {
it('collapses agent detail navigation from the top-right toggle', async () => {
mockPathname = '/roster/agent/agent-1/configure'
renderMainNav()
fireEvent.click(screen.getByTestId('agent-detail-toggle'))
fireEvent.click(await screen.findByTestId('agent-detail-toggle'))
expect(screen.getByRole('complementary')).toHaveClass('w-16')
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'false')
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'false')
expect(await screen.findByRole('navigation', { name: 'agentV2.agentDetail.navigationLabel' })).toHaveClass('px-3')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
})
@ -923,18 +925,18 @@ describe('MainNav', () => {
)
})
it('shows agent detail navigation as a floating preview when hovering the collapsed top toggle', () => {
it('shows agent detail navigation as a floating preview when hovering the collapsed top toggle', async () => {
mockPathname = '/roster/agent/agent-1/configure'
renderMainNav()
fireEvent.click(screen.getByTestId('agent-detail-toggle'))
fireEvent.click(await screen.findByTestId('agent-detail-toggle'))
fireEvent.mouseEnter(screen.getByTestId('agent-detail-top').parentElement!)
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
expect(localStorage.getItem(DETAIL_SIDEBAR_STORAGE_KEY)).toBe('collapse')
expect(screen.getAllByTestId('agent-detail-top')).toHaveLength(1)
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')
expect(await screen.findByRole('navigation', { name: 'agentV2.agentDetail.navigationLabel' })).toHaveClass('px-1')
})
it.each([
@ -1258,12 +1260,12 @@ describe('MainNav', () => {
}
})
it('collapses and expands installed web apps from the section arrow', () => {
it('collapses and expands installed web apps from the section arrow', async () => {
mockInstalledApps = [createInstalledApp()]
renderMainNav()
const webAppsButton = screen.getByRole('button', { name: 'explore.sidebar.webApps' })
const webAppsButton = await screen.findByRole('button', { name: 'explore.sidebar.webApps' })
expect(webAppsButton).toHaveAttribute('aria-expanded', 'true')
expect(screen.getByText('Alpha App')).toBeInTheDocument()

View File

@ -0,0 +1,35 @@
'use client'
import { useShallow } from 'zustand/react/shallow'
import { SnippetCollapsedPreview } from '@/app/components/snippets/components/snippet-collapsed-preview'
import { SnippetSidebarContent } from '@/app/components/snippets/components/snippet-sidebar'
import { useSnippetDraftStore } from '@/app/components/snippets/draft-store'
import { useSnippetDetailStore } from '@/app/components/snippets/store'
type SnippetDetailSectionProps = {
expand: boolean
}
export function SnippetDetailSection({ expand }: SnippetDetailSectionProps) {
const snippetNavigation = useSnippetDetailStore(useShallow(state => ({
onFieldsChange: state.onFieldsChange,
readonly: state.readonly,
snippet: state.snippet,
})))
const snippetInputFields = useSnippetDraftStore(state => state.inputFields)
if (!expand)
return <SnippetCollapsedPreview inputFieldCount={snippetInputFields.length} />
if (!snippetNavigation.snippet || !snippetNavigation.onFieldsChange)
return null
return (
<SnippetSidebarContent
snippet={snippetNavigation.snippet}
fields={snippetInputFields}
readonly={snippetNavigation.readonly}
onFieldsChange={snippetNavigation.onFieldsChange}
/>
)
}

View File

@ -7,31 +7,21 @@ import { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import AppDetailSection from '@/app/components/app-sidebar/app-detail-section'
import AppDetailTop from '@/app/components/app-sidebar/app-detail-top'
import DatasetDetailSection from '@/app/components/app-sidebar/dataset-detail-section'
import DatasetDetailTop from '@/app/components/app-sidebar/dataset-detail-top'
import { useStore as useAppStore } from '@/app/components/app/store'
import Badge from '@/app/components/base/badge'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import EnvNav from '@/app/components/header/env-nav'
import { SnippetCollapsedPreview } from '@/app/components/snippets/components/snippet-collapsed-preview'
import { SnippetSidebarContent } from '@/app/components/snippets/components/snippet-sidebar'
import { useSnippetDraftStore } from '@/app/components/snippets/draft-store'
import { useSnippetDetailStore } from '@/app/components/snippets/store'
import { useAppContext } from '@/context/app-context'
import { AgentDetailSection, AgentDetailTop } from '@/features/agent-v2/agent-detail/navigation'
import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag'
import { DeploymentDetailSection, DeploymentDetailTop } from '@/features/deployments/detail/deployment-sidebar'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import dynamic from '@/next/dynamic'
import Link from '@/next/link'
import { usePathname } from '@/next/navigation'
import AccountSection from './components/account-section'
import HelpMenu from './components/help-menu'
import MainNavLink from './components/nav-link'
import { MainNavSearchButton } from './components/search-button'
import SnippetDetailTop from './components/snippet-detail-top'
import WebAppsSection from './components/web-apps-section'
import { WorkspaceCard } from './components/workspace-card'
import { isMainNavRouteVisible, MAIN_NAV_ROUTES } from './routes'
import { useDetailSidebarMode } from './storage'
@ -41,6 +31,16 @@ const DATASET_DOCUMENT_CREATION_ROUTES = new Set(['create', 'create-from-pipelin
const DEPLOYMENT_COLLECTION_ROUTES = new Set(['create'])
const secondarySidebarHelpTriggerIcon = <span aria-hidden className="i-ri-question-line size-4 shrink-0" />
const AppDetailSection = dynamic(() => import('@/app/components/app-sidebar/app-detail-section'), { ssr: false })
const AppDetailTop = dynamic(() => import('@/app/components/app-sidebar/app-detail-top'), { ssr: false })
const DatasetDetailSection = dynamic(() => import('@/app/components/app-sidebar/dataset-detail-section'), { ssr: false })
const DatasetDetailTop = dynamic(() => import('@/app/components/app-sidebar/dataset-detail-top'), { ssr: false })
const AgentDetailSection = dynamic(() => import('@/features/agent-v2/agent-detail/navigation').then(mod => mod.AgentDetailSection), { ssr: false })
const AgentDetailTop = dynamic(() => import('@/features/agent-v2/agent-detail/navigation').then(mod => mod.AgentDetailTop), { ssr: false })
const SnippetDetailTop = dynamic(() => import('./components/snippet-detail-top'), { ssr: false })
const SnippetDetailSection = dynamic(() => import('./components/snippet-detail-section').then(mod => mod.SnippetDetailSection), { ssr: false })
const WebAppsSection = dynamic(() => import('./components/web-apps-section'), { ssr: false })
function SecondarySidebarHelpMenu({
triggerClassName,
}: {
@ -103,12 +103,6 @@ export function MainNav({
const showDeploymentDetailNavigation = canUseAppDeploy && !isCurrentWorkspaceDatasetOperator && isDeploymentDetailPathname(pathname)
const showSnippetDetailNavigation = isSnippetDetailPathname(pathname)
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation || showAgentDetailNavigation || showDeploymentDetailNavigation || showSnippetDetailNavigation
const snippetNavigation = useSnippetDetailStore(useShallow(state => ({
onFieldsChange: state.onFieldsChange,
readonly: state.readonly,
snippet: state.snippet,
})))
const snippetInputFields = useSnippetDraftStore(state => state.inputFields)
const { hasAppDetail, setAppDetail } = useAppStore(useShallow(state => ({
hasAppDetail: !!state.appDetail,
setAppDetail: state.setAppDetail,
@ -307,18 +301,7 @@ export function MainNav({
? <AgentDetailSection expand={detailNavigationVisibleExpanded} />
: showDeploymentDetailNavigation
? <DeploymentDetailSection expand={detailNavigationVisibleExpanded} />
: detailNavigationVisibleExpanded
? snippetNavigation.snippet && snippetNavigation.onFieldsChange
? (
<SnippetSidebarContent
snippet={snippetNavigation.snippet}
fields={snippetInputFields}
readonly={snippetNavigation.readonly}
onFieldsChange={snippetNavigation.onFieldsChange}
/>
)
: null
: <SnippetCollapsedPreview inputFieldCount={snippetInputFields.length} />
: <SnippetDetailSection expand={detailNavigationVisibleExpanded} />
: (
<>
<nav className="isolate flex flex-col gap-px p-2">

View File

@ -2,10 +2,10 @@
import type { Resource } from 'i18next'
import type { Locale } from '.'
import type { Namespace, NamespaceInFileName } from './resources'
import { kebabCase } from 'es-toolkit/string'
import { createInstance } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { getI18n, initReactI18next } from 'react-i18next'
import { loadI18nResource } from './load-resource'
import { getInitOptions } from './settings'
export function createI18nextInstance(lng: Locale, resources: Resource) {
@ -15,10 +15,7 @@ export function createI18nextInstance(lng: Locale, resources: Resource) {
.use(resourcesToBackend((
language: Locale,
namespace: NamespaceInFileName | Namespace,
) => {
const namespaceKebab = kebabCase(namespace)
return import(`../i18n/${language}/${namespaceKebab}.json`)
}))
) => loadI18nResource(language, namespace)))
.init({
...getInitOptions(),
lng,

View File

@ -0,0 +1,35 @@
import type { ResourceKey } from 'i18next'
import type { Locale } from './language'
import type { Namespace, NamespaceInFileName } from './resources'
import { kebabCase } from 'es-toolkit/string'
import { LanguagesSupported } from './language'
type LocaleResourceModule = {
loadResource: (fileNamespace: string) => Promise<{ default: ResourceKey }>
}
const legacyLocaleMap: Partial<Record<Locale, Locale>> = {
en_US: 'en-US',
ja_JP: 'ja-JP',
zh_Hans: 'zh-Hans',
}
const defaultLocale = 'en-US' satisfies Locale
const normalizeLocale = (locale: Locale): Locale => {
const normalized = legacyLocaleMap[locale] ?? locale
if (LanguagesSupported.includes(normalized))
return normalized
return defaultLocale
}
const loadLocaleResources = (locale: Locale): Promise<LocaleResourceModule> => {
const normalized = normalizeLocale(locale)
return import(`./locale-resources/${normalized}.ts`)
}
export const loadI18nResource = async (locale: Locale, namespace: Namespace | NamespaceInFileName) => {
const { loadResource } = await loadLocaleResources(locale)
return loadResource(kebabCase(namespace))
}

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/ar-TN/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/de-DE/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/en-US/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/es-ES/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/fa-IR/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/fr-FR/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/hi-IN/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/id-ID/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/it-IT/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/ja-JP/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/ko-KR/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/nl-NL/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/pl-PL/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/pt-BR/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/ro-RO/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/ru-RU/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/sl-SI/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/th-TH/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/tr-TR/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/uk-UA/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/vi-VN/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/zh-Hans/${fileNamespace}.json`)

View File

@ -0,0 +1 @@
export const loadResource = (fileNamespace: string) => import(`../../i18n/zh-Hant/${fileNamespace}.json`)

View File

@ -2,7 +2,6 @@ import type { i18n as I18nInstance, Resource, ResourceLanguage } from 'i18next'
import type { Locale } from '.'
import type { Namespace, NamespaceInFileName } from './resources'
import { match } from '@formatjs/intl-localematcher'
import { kebabCase } from 'es-toolkit/compat'
import { camelCase } from 'es-toolkit/string'
import { createInstance } from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
@ -12,6 +11,7 @@ import { initReactI18next } from 'react-i18next/initReactI18next'
import { cookies, headers } from '@/next/headers'
import { serverOnlyContext } from '@/utils/server-only-context'
import { i18n } from '.'
import { loadI18nResource } from './load-resource'
import { namespacesInFileName } from './resources'
import { getInitOptions } from './settings'
@ -26,10 +26,7 @@ const getOrCreateI18next = async (lng: Locale) => {
instance = createInstance()
await instance
.use(initReactI18next)
.use(resourcesToBackend((language: Locale, namespace: Namespace | NamespaceInFileName) => {
const fileNamespace = kebabCase(namespace)
return import(`../i18n/${language}/${fileNamespace}.json`)
}))
.use(resourcesToBackend((language: Locale, namespace: Namespace | NamespaceInFileName) => loadI18nResource(language, namespace)))
.init({
...getInitOptions(),
lng,
@ -85,7 +82,7 @@ export const getResources = cache(async (lng: Locale): Promise<Resource> => {
await Promise.all(
(namespacesInFileName).map(async (ns) => {
const mod = await import(`../i18n/${lng}/${ns}.json`)
const mod = await loadI18nResource(lng, ns)
messages[camelCase(ns)] = mod.default
}),
)

View File

@ -35,6 +35,7 @@ const config: KnipConfig = {
ignoreFiles: [
'features/agent-v2/agent-detail/configure/components/orchestrate/memory.tsx',
'features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/option-menu.tsx',
'i18n-config/locale-resources/*.ts',
],
ignoreBinaries: [
'only-allow',