feat: localize docs anchors and correct workflow node doc links

This commit is contained in:
yyh
2026-02-13 10:24:44 +08:00
parent 40078108de
commit 5e9f3eab8b
6 changed files with 96 additions and 14 deletions

View File

@@ -16,6 +16,7 @@ import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
import { appManagementAnchorMap } from '@/context/doc-anchors'
import { useDocLink } from '@/context/i18n'
import { useProviderContext } from '@/context/provider-context'
import {
@@ -313,7 +314,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
<div className="flex items-center justify-between px-6 pb-6 pt-5">
<a
className="flex items-center gap-1 text-text-accent system-xs-regular"
href={docLink('/use-dify/workspace/app-management#app-export-and-import')}
href={docLink('/use-dify/workspace/app-management#app-export-and-import', undefined, appManagementAnchorMap)}
target="_blank"
rel="noopener noreferrer"
>

View File

@@ -1,7 +1,7 @@
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import type { CommonNodeType, NodeDefault, NodeDefaultBase } from '@/app/components/workflow/types'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import { useMemo } from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useFeatures } from '@/app/components/base/features/hooks'
@@ -16,6 +16,10 @@ import { BlockEnum } from '@/app/components/workflow/types'
import { useDocLink } from '@/context/i18n'
import { useIsChatMode } from './use-is-chat-mode'
const NODE_HELP_LINK_OVERRIDES: Partial<Record<BlockEnum, string>> = {
[BlockEnum.FileUpload]: 'upload-file-to-sandbox',
}
export const useAvailableNodesMetaData = () => {
const { t } = useTranslation()
const isChatMode = useIsChatMode()
@@ -49,6 +53,13 @@ export const useAvailableNodesMetaData = () => {
),
] as AvailableNodesMetaData['nodes'], [isChatMode, isSandboxed, startNodeMetaData])
const getHelpLinkSlug = useCallback((nodeType: BlockEnum, helpLinkUri?: string) => {
if (isSandboxed && nodeType === BlockEnum.LLM)
return BlockEnum.Agent
return NODE_HELP_LINK_OVERRIDES[nodeType] || helpLinkUri || nodeType
}, [isSandboxed])
const availableNodesMetaData = useMemo<NodeDefaultBase[]>(() => {
const toNodeDefaultBase = (
node: NodeDefault<CommonNodeType>,
@@ -83,7 +94,7 @@ export const useAvailableNodesMetaData = () => {
? BlockEnum.Agent
: undefined
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
const helpLinkPath = `/use-dify/nodes/${metaData.helpLinkUri}` as DocPathWithoutLang
const helpLinkPath = `/use-dify/nodes/${getHelpLinkSlug(metaData.type, metaData.helpLinkUri)}` as DocPathWithoutLang
return toNodeDefaultBase(typedNode, {
...metaData,
iconType: iconTypeOverride,
@@ -97,7 +108,7 @@ export const useAvailableNodesMetaData = () => {
_iconTypeOverride: iconTypeOverride,
})
})
}, [mergedNodesMetaData, t, docLink, isSandboxed])
}, [mergedNodesMetaData, t, docLink, isSandboxed, getHelpLinkSlug])
const availableNodesMetaDataMap = useMemo(() => availableNodesMetaData.reduce((acc, node) => {
acc![node.metaData.type] = node

View File

@@ -1,4 +1,5 @@
import type { InspectHeaderProps } from './inspect-layout'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import type { SandboxFileTreeNode } from '@/types/sandbox-file'
import {
RiCloseLine,
@@ -12,6 +13,7 @@ import { FileDownload01 } from '@/app/components/base/icons/src/vender/line/file
import Loading from '@/app/components/base/loading'
import ArtifactsTree from '@/app/components/workflow/skill/file-tree/artifacts/artifacts-tree'
import ReadOnlyFilePreview from '@/app/components/workflow/skill/viewer/read-only-file-preview'
import { fileSystemArtifactsAnchorMap } from '@/context/doc-anchors'
import { useDocLink } from '@/context/i18n'
import { useDownloadSandboxFile, useSandboxFileDownloadUrl, useSandboxFilesTree } from '@/service/use-sandbox-file'
import { cn } from '@/utils/classnames'
@@ -35,7 +37,7 @@ const ArtifactsEmpty = ({ description }: { description: string }) => {
<div className="text-text-tertiary system-xs-regular">{description}</div>
<a
className="cursor-pointer text-text-accent system-xs-regular"
href={docLink('/use-dify/debug/variable-inspect')}
href={docLink('/use-dify/build/file-system#artifacts' as DocPathWithoutLang, undefined, fileSystemArtifactsAnchorMap)}
target="_blank"
rel="noopener noreferrer"
>

View File

@@ -0,0 +1,15 @@
import type { DocAnchorMap } from './i18n'
export const appManagementAnchorMap = {
'zh-Hans': '应用导出和导入',
'zh_Hans': '应用导出和导入',
'ja-JP': 'アプリのエクスポートとインポート',
'ja_JP': 'アプリのエクスポートとインポート',
} satisfies DocAnchorMap
export const fileSystemArtifactsAnchorMap = {
'zh-Hans': '产物',
'zh_Hans': '产物',
'ja-JP': 'アーティファクト',
'ja_JP': 'アーティファクト',
} satisfies DocAnchorMap

View File

@@ -1,4 +1,4 @@
import type { DocPathMap } from './i18n'
import type { DocAnchorMap, DocPathMap } from './i18n'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import { useTranslation } from '#i18n'
import { renderHook } from '@testing-library/react'
@@ -234,6 +234,17 @@ describe('useDocLink', () => {
const url = result.current('/use-dify/getting-started/introduction')
expect(url).toBe(`${defaultDocBaseUrl}/zh/use-dify/getting-started/introduction`)
})
it('should preserve anchor while translating API reference path', () => {
vi.mocked(useTranslation).mockReturnValue({
i18n: { language: 'zh-Hans' },
} as ReturnType<typeof useTranslation>)
vi.mocked(getDocLanguage).mockReturnValue('zh')
const { result } = renderHook(() => useDocLink())
const url = result.current('/api-reference/annotations/create-annotation#request-body')
expect(url).toBe(`${defaultDocBaseUrl}/api-reference/标注管理/创建标注#request-body`)
})
})
describe('Edge Cases', () => {
@@ -243,6 +254,22 @@ describe('useDocLink', () => {
expect(url).toBe(`${defaultDocBaseUrl}/en/use-dify/getting-started/introduction#overview`)
})
it('should support locale-specific anchors via anchorMap', () => {
vi.mocked(useTranslation).mockReturnValue({
i18n: { language: 'zh-Hans' },
} as ReturnType<typeof useTranslation>)
vi.mocked(getDocLanguage).mockReturnValue('zh')
const anchorMap: DocAnchorMap = {
'zh-Hans': '应用导出和导入',
'ja-JP': 'アプリのエクスポートとインポート',
}
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/workspace/app-management#app-export-and-import', undefined, anchorMap)
expect(url).toBe(`${defaultDocBaseUrl}/zh/use-dify/workspace/app-management#${encodeURIComponent('应用导出和导入')}`)
})
it('should handle multiple calls with same hook instance', () => {
const { result } = renderHook(() => useDocLink())
const url1 = result.current('/use-dify/getting-started/introduction')

View File

@@ -21,31 +21,57 @@ export const useGetPricingPageLanguage = () => {
return getPricingPageLanguage(locale)
}
export const defaultDocBaseUrl = 'https://dify-6c0370d8-add-new-agent.mintlify.app'
export const defaultDocBaseUrl = 'https://docs.bash-is-all-you-need.dify.dev'
export type DocPathMap = Partial<Record<Locale, DocPathWithoutLang>>
export type DocAnchorMap = Partial<Record<Locale, string>>
export const useDocLink = (baseUrl?: string): ((path?: DocPathWithoutLang, pathMap?: DocPathMap) => string) => {
const splitPathWithHash = (path: string) => {
const [pathname, ...hashParts] = path.split('#')
return {
pathname,
hash: hashParts.join('#'),
}
}
const normalizeAnchor = (anchor: string) => {
const normalizedAnchor = anchor.startsWith('#') ? anchor.slice(1) : anchor
if (!normalizedAnchor)
return ''
const isAsciiOnly = Array.from(normalizedAnchor).every(char => char.codePointAt(0)! <= 0x7F)
if (isAsciiOnly)
return normalizedAnchor
return encodeURIComponent(normalizedAnchor)
}
export const useDocLink = (baseUrl?: string): ((path?: DocPathWithoutLang, pathMap?: DocPathMap, anchorMap?: DocAnchorMap) => string) => {
let baseDocUrl = baseUrl || defaultDocBaseUrl
baseDocUrl = (baseDocUrl.endsWith('/')) ? baseDocUrl.slice(0, -1) : baseDocUrl
const locale = useLocale()
return useCallback(
(path?: DocPathWithoutLang, pathMap?: DocPathMap): string => {
(path?: DocPathWithoutLang, pathMap?: DocPathMap, anchorMap?: DocAnchorMap): string => {
const docLanguage = getDocLanguage(locale)
const pathUrl = path || ''
let targetPath = (pathMap) ? pathMap[locale] || pathUrl : pathUrl
const targetPath = (pathMap) ? pathMap[locale] || pathUrl : pathUrl
const { pathname: pathWithoutHash, hash: pathAnchor } = splitPathWithHash(targetPath)
let targetPathWithoutHash = pathWithoutHash
let languagePrefix = `/${docLanguage}`
if (targetPath.startsWith('/api-reference/')) {
if (targetPathWithoutHash.startsWith('/api-reference/')) {
languagePrefix = ''
if (docLanguage !== 'en') {
const translatedPath = apiReferencePathTranslations[targetPath]?.[docLanguage]
const translatedPath = apiReferencePathTranslations[targetPathWithoutHash]?.[docLanguage]
if (translatedPath) {
targetPath = translatedPath
targetPathWithoutHash = translatedPath
}
}
}
return `${baseDocUrl}${languagePrefix}${targetPath}`
const anchor = normalizeAnchor(anchorMap?.[locale] || pathAnchor)
const anchorSuffix = anchor ? `#${anchor}` : ''
return `${baseDocUrl}${languagePrefix}${targetPathWithoutHash}${anchorSuffix}`
},
[baseDocUrl, locale],
)