From 5e9f3eab8ba30a79c6d69e57a5fbab1dc7ed43fd Mon Sep 17 00:00:00 2001 From: yyh Date: Fri, 13 Feb 2026 10:24:44 +0800 Subject: [PATCH] feat: localize docs anchors and correct workflow node doc links --- .../app/create-from-dsl-modal/index.tsx | 3 +- .../hooks/use-available-nodes-meta-data.ts | 17 ++++++-- .../variable-inspect/artifacts-tab.tsx | 4 +- web/context/doc-anchors.ts | 15 +++++++ web/context/i18n.spec.ts | 29 ++++++++++++- web/context/i18n.ts | 42 +++++++++++++++---- 6 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 web/context/doc-anchors.ts diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index 9d86685eff..8a3e95207a 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -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
diff --git a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts index eec5addee3..c7ca27331d 100644 --- a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts +++ b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts @@ -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> = { + [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(() => { const toNodeDefaultBase = ( node: NodeDefault, @@ -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 diff --git a/web/app/components/workflow/variable-inspect/artifacts-tab.tsx b/web/app/components/workflow/variable-inspect/artifacts-tab.tsx index 34b78e274f..0f595ed287 100644 --- a/web/app/components/workflow/variable-inspect/artifacts-tab.tsx +++ b/web/app/components/workflow/variable-inspect/artifacts-tab.tsx @@ -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 }) => {
{description}
diff --git a/web/context/doc-anchors.ts b/web/context/doc-anchors.ts new file mode 100644 index 0000000000..7325025aca --- /dev/null +++ b/web/context/doc-anchors.ts @@ -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 diff --git a/web/context/i18n.spec.ts b/web/context/i18n.spec.ts index 616f3bfced..4212fe0317 100644 --- a/web/context/i18n.spec.ts +++ b/web/context/i18n.spec.ts @@ -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) + 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) + 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') diff --git a/web/context/i18n.ts b/web/context/i18n.ts index 64b13f5867..7b970ae9c9 100644 --- a/web/context/i18n.ts +++ b/web/context/i18n.ts @@ -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> +export type DocAnchorMap = Partial> -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], )