fix(windows): normalize Windows file paths with leading slash and add i18n to link safety dialog

Strip spurious leading slash from Windows drive-letter paths (/D:/foo → D:/foo) in parseLocalFileTarget so that workspace-relative comparison succeeds and local files can be opened correctly. Display the normalized path in the confirmation dialog. Internationalize all link safety dialog and toast strings across 10 locales.
This commit is contained in:
xintaofei
2026-04-13 22:47:14 +08:00
parent b76dc63e77
commit 0a39e1daf4
11 changed files with 184 additions and 19 deletions

View File

@@ -1,6 +1,7 @@
"use client"
import { useCallback, useMemo, useState } from "react"
import { useTranslations } from "next-intl"
import { openUrl } from "@/lib/platform"
import type { LinkSafetyConfig, LinkSafetyModalProps } from "streamdown"
import { toast } from "sonner"
@@ -29,6 +30,14 @@ function normalizeSlashPath(path: string): string {
return path.replace(/\\/g, "/")
}
/** Strip leading slash before Windows drive letter: /C:/foo → C:/foo */
function stripLeadingSlashOnWindows(p: string): string {
if (p.startsWith("/") && WINDOWS_ABSOLUTE_PATH.test(p.slice(1))) {
return p.slice(1)
}
return p
}
function decodeUriSafely(value: string): string {
try {
return decodeURIComponent(value)
@@ -92,10 +101,7 @@ function parseLocalFileTarget(rawUrl: string): LocalFileTarget | null {
try {
const parsed = new URL(raw)
const rawPathname = decodeUriSafely(parsed.pathname)
const normalizedPathname =
rawPathname.startsWith("/") && WINDOWS_ABSOLUTE_PATH.test(rawPathname)
? rawPathname.slice(1)
: rawPathname
const normalizedPathname = stripLeadingSlashOnWindows(rawPathname)
const pathAndLine = splitPathAndLine(normalizedPathname)
if (!pathAndLine.path) return null
return {
@@ -118,10 +124,11 @@ function parseLocalFileTarget(rawUrl: string): LocalFileTarget | null {
const withoutQuery =
queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash
const pathAndLine = splitPathAndLine(withoutQuery)
if (!isLocalPathLike(pathAndLine.path)) return null
const normalizedPath = stripLeadingSlashOnWindows(pathAndLine.path)
if (!isLocalPathLike(normalizedPath)) return null
return {
path: normalizeSlashPath(pathAndLine.path),
path: normalizeSlashPath(normalizedPath),
line: parseHashLine(hash) ?? pathAndLine.line,
}
}
@@ -163,6 +170,7 @@ function LinkSafetyModal({
}: LinkSafetyModalProps & {
onAction: (url: string) => Promise<void>
}) {
const t = useTranslations("Folder.chat.linkSafety")
const [opening, setOpening] = useState(false)
const localTarget = useMemo(() => parseLocalFileTarget(url), [url])
const isLocalFile = Boolean(localTarget)
@@ -185,21 +193,27 @@ function LinkSafetyModal({
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{isLocalFile ? "Open local file?" : "Open external link?"}
{isLocalFile ? t("localFileTitle") : t("externalLinkTitle")}
</AlertDialogTitle>
<AlertDialogDescription>
{isLocalFile
? "You're about to open a local file in the Files panel."
: "You're about to visit an external website."}
? t("localFileDescription")
: t("externalLinkDescription")}
</AlertDialogDescription>
</AlertDialogHeader>
<div className="max-h-28 overflow-auto rounded-md bg-muted px-3 py-2 font-mono text-xs break-all">
{url}
{localTarget?.path ?? url}
</div>
<AlertDialogFooter>
<AlertDialogCancel disabled={opening}>Cancel</AlertDialogCancel>
<AlertDialogCancel disabled={opening}>
{t("cancel")}
</AlertDialogCancel>
<AlertDialogAction disabled={opening} onClick={handleAction}>
{opening ? "Opening..." : isLocalFile ? "Open file" : "Open link"}
{opening
? t("opening")
: isLocalFile
? t("openFile")
: t("openLink")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -208,6 +222,7 @@ function LinkSafetyModal({
}
export function useStreamdownLinkSafety(): LinkSafetyConfig {
const t = useTranslations("Folder.chat.linkSafety")
const { folder } = useFolderContext()
const folderPath = folder?.path
const { openFilePreview } = useWorkspaceContext()
@@ -217,8 +232,8 @@ export function useStreamdownLinkSafety(): LinkSafetyConfig {
const localTarget = parseLocalFileTarget(url)
if (localTarget) {
if (!folderPath) {
toast.error("Cannot open local file", {
description: "No workspace folder is currently active.",
toast.error(t("errorCannotOpen"), {
description: t("errorNoWorkspace"),
})
return
}
@@ -228,8 +243,8 @@ export function useStreamdownLinkSafety(): LinkSafetyConfig {
folderPath
)
if (!relativePath) {
toast.error("Cannot open local file", {
description: "The file is outside the current workspace folder.",
toast.error(t("errorCannotOpen"), {
description: t("errorOutsideWorkspace"),
})
return
}
@@ -239,7 +254,7 @@ export function useStreamdownLinkSafety(): LinkSafetyConfig {
line: localTarget.line ?? undefined,
})
} catch (error) {
toast.error("Failed to open local file", {
toast.error(t("errorFailedOpen"), {
description: error instanceof Error ? error.message : String(error),
})
}
@@ -249,12 +264,12 @@ export function useStreamdownLinkSafety(): LinkSafetyConfig {
try {
await openUrl(url)
} catch (error) {
toast.error("Failed to open link", {
toast.error(t("errorFailedLink"), {
description: error instanceof Error ? error.message : String(error),
})
}
},
[folderPath, openFilePreview]
[folderPath, openFilePreview, t]
)
const renderModal = useCallback(

View File

@@ -1556,6 +1556,21 @@
"thoughtForFewSeconds": "تفكير",
"thoughtForSeconds": "تفكير"
},
"linkSafety": {
"localFileTitle": "فتح ملف محلي؟",
"externalLinkTitle": "فتح رابط خارجي؟",
"localFileDescription": "أنت على وشك فتح ملف محلي في لوحة الملفات.",
"externalLinkDescription": "أنت على وشك زيارة موقع ويب خارجي.",
"cancel": "إلغاء",
"opening": "جارٍ الفتح…",
"openFile": "فتح الملف",
"openLink": "فتح الرابط",
"errorCannotOpen": "تعذر فتح الملف المحلي",
"errorNoWorkspace": "لا يوجد مجلد مساحة عمل نشط حالياً.",
"errorOutsideWorkspace": "الملف خارج مجلد مساحة العمل الحالي.",
"errorFailedOpen": "فشل فتح الملف المحلي",
"errorFailedLink": "فشل فتح الرابط"
},
"messageList": {
"attachedResources": "الموارد المرفقة",
"loading": "جارٍ التحميل...",

View File

@@ -1556,6 +1556,21 @@
"thoughtForFewSeconds": "Nachgedacht",
"thoughtForSeconds": "Nachgedacht"
},
"linkSafety": {
"localFileTitle": "Lokale Datei öffnen?",
"externalLinkTitle": "Externen Link öffnen?",
"localFileDescription": "Sie sind dabei, eine lokale Datei im Dateipanel zu öffnen.",
"externalLinkDescription": "Sie sind dabei, eine externe Website zu besuchen.",
"cancel": "Abbrechen",
"opening": "Wird geöffnet…",
"openFile": "Datei öffnen",
"openLink": "Link öffnen",
"errorCannotOpen": "Lokale Datei kann nicht geöffnet werden",
"errorNoWorkspace": "Es ist kein Arbeitsbereichsordner aktiv.",
"errorOutsideWorkspace": "Die Datei befindet sich außerhalb des aktuellen Arbeitsbereichsordners.",
"errorFailedOpen": "Lokale Datei konnte nicht geöffnet werden",
"errorFailedLink": "Link konnte nicht geöffnet werden"
},
"messageList": {
"attachedResources": "Angehängte Ressourcen",
"loading": "Lädt...",

View File

@@ -1556,6 +1556,21 @@
"thoughtForFewSeconds": "Thought",
"thoughtForSeconds": "Thought"
},
"linkSafety": {
"localFileTitle": "Open local file?",
"externalLinkTitle": "Open external link?",
"localFileDescription": "You're about to open a local file in the Files panel.",
"externalLinkDescription": "You're about to visit an external website.",
"cancel": "Cancel",
"opening": "Opening...",
"openFile": "Open file",
"openLink": "Open link",
"errorCannotOpen": "Cannot open local file",
"errorNoWorkspace": "No workspace folder is currently active.",
"errorOutsideWorkspace": "The file is outside the current workspace folder.",
"errorFailedOpen": "Failed to open local file",
"errorFailedLink": "Failed to open link"
},
"messageList": {
"attachedResources": "Attached resources",
"loading": "Loading...",

View File

@@ -1556,6 +1556,21 @@
"thoughtForFewSeconds": "Pensamiento",
"thoughtForSeconds": "Pensamiento"
},
"linkSafety": {
"localFileTitle": "¿Abrir archivo local?",
"externalLinkTitle": "¿Abrir enlace externo?",
"localFileDescription": "Está a punto de abrir un archivo local en el panel de archivos.",
"externalLinkDescription": "Está a punto de visitar un sitio web externo.",
"cancel": "Cancelar",
"opening": "Abriendo…",
"openFile": "Abrir archivo",
"openLink": "Abrir enlace",
"errorCannotOpen": "No se puede abrir el archivo local",
"errorNoWorkspace": "No hay ninguna carpeta de espacio de trabajo activa.",
"errorOutsideWorkspace": "El archivo está fuera de la carpeta del espacio de trabajo actual.",
"errorFailedOpen": "Error al abrir el archivo local",
"errorFailedLink": "Error al abrir el enlace"
},
"messageList": {
"attachedResources": "Recursos adjuntos",
"loading": "Cargando...",

View File

@@ -1556,6 +1556,21 @@
"thoughtForFewSeconds": "Réflexion",
"thoughtForSeconds": "Réflexion"
},
"linkSafety": {
"localFileTitle": "Ouvrir le fichier local ?",
"externalLinkTitle": "Ouvrir le lien externe ?",
"localFileDescription": "Vous êtes sur le point d'ouvrir un fichier local dans le panneau de fichiers.",
"externalLinkDescription": "Vous êtes sur le point de visiter un site web externe.",
"cancel": "Annuler",
"opening": "Ouverture…",
"openFile": "Ouvrir le fichier",
"openLink": "Ouvrir le lien",
"errorCannotOpen": "Impossible d'ouvrir le fichier local",
"errorNoWorkspace": "Aucun dossier d'espace de travail n'est actuellement actif.",
"errorOutsideWorkspace": "Le fichier se trouve en dehors du dossier de l'espace de travail actuel.",
"errorFailedOpen": "Échec de l'ouverture du fichier local",
"errorFailedLink": "Échec de l'ouverture du lien"
},
"messageList": {
"attachedResources": "Ressources jointes",
"loading": "Chargement...",

View File

@@ -1556,6 +1556,21 @@
"thoughtForFewSeconds": "考えた",
"thoughtForSeconds": "考えた"
},
"linkSafety": {
"localFileTitle": "ローカルファイルを開きますか?",
"externalLinkTitle": "外部リンクを開きますか?",
"localFileDescription": "ファイルパネルでローカルファイルを開こうとしています。",
"externalLinkDescription": "外部ウェブサイトにアクセスしようとしています。",
"cancel": "キャンセル",
"opening": "開いています…",
"openFile": "ファイルを開く",
"openLink": "リンクを開く",
"errorCannotOpen": "ローカルファイルを開けません",
"errorNoWorkspace": "現在アクティブなワークスペースフォルダがありません。",
"errorOutsideWorkspace": "ファイルが現在のワークスペースフォルダの外にあります。",
"errorFailedOpen": "ローカルファイルを開けませんでした",
"errorFailedLink": "リンクを開けませんでした"
},
"messageList": {
"attachedResources": "添付リソース",
"loading": "読み込み中...",

View File

@@ -1556,6 +1556,21 @@
"thoughtForFewSeconds": "생각함",
"thoughtForSeconds": "생각함"
},
"linkSafety": {
"localFileTitle": "로컬 파일을 여시겠습니까?",
"externalLinkTitle": "외부 링크를 여시겠습니까?",
"localFileDescription": "파일 패널에서 로컬 파일을 열려고 합니다.",
"externalLinkDescription": "외부 웹사이트를 방문하려고 합니다.",
"cancel": "취소",
"opening": "열고 있습니다…",
"openFile": "파일 열기",
"openLink": "링크 열기",
"errorCannotOpen": "로컬 파일을 열 수 없습니다",
"errorNoWorkspace": "현재 활성화된 워크스페이스 폴더가 없습니다.",
"errorOutsideWorkspace": "파일이 현재 워크스페이스 폴더 밖에 있습니다.",
"errorFailedOpen": "로컬 파일 열기 실패",
"errorFailedLink": "링크 열기 실패"
},
"messageList": {
"attachedResources": "첨부된 리소스",
"loading": "불러오는 중...",

View File

@@ -1556,6 +1556,21 @@
"thoughtForFewSeconds": "Pensamento",
"thoughtForSeconds": "Pensamento"
},
"linkSafety": {
"localFileTitle": "Abrir arquivo local?",
"externalLinkTitle": "Abrir link externo?",
"localFileDescription": "Você está prestes a abrir um arquivo local no painel de arquivos.",
"externalLinkDescription": "Você está prestes a visitar um site externo.",
"cancel": "Cancelar",
"opening": "Abrindo…",
"openFile": "Abrir arquivo",
"openLink": "Abrir link",
"errorCannotOpen": "Não é possível abrir o arquivo local",
"errorNoWorkspace": "Nenhuma pasta de espaço de trabalho está ativa no momento.",
"errorOutsideWorkspace": "O arquivo está fora da pasta do espaço de trabalho atual.",
"errorFailedOpen": "Falha ao abrir o arquivo local",
"errorFailedLink": "Falha ao abrir o link"
},
"messageList": {
"attachedResources": "Recursos anexados",
"loading": "Carregando...",

View File

@@ -1556,6 +1556,21 @@
"thoughtForFewSeconds": "思考",
"thoughtForSeconds": "思考"
},
"linkSafety": {
"localFileTitle": "打开本地文件?",
"externalLinkTitle": "打开外部链接?",
"localFileDescription": "即将在文件面板中打开一个本地文件。",
"externalLinkDescription": "即将访问一个外部网站。",
"cancel": "取消",
"opening": "正在打开…",
"openFile": "打开文件",
"openLink": "打开链接",
"errorCannotOpen": "无法打开本地文件",
"errorNoWorkspace": "当前没有活跃的工作区文件夹。",
"errorOutsideWorkspace": "该文件不在当前工作区文件夹内。",
"errorFailedOpen": "打开本地文件失败",
"errorFailedLink": "打开链接失败"
},
"messageList": {
"attachedResources": "附加资源",
"loading": "加载中...",

View File

@@ -1556,6 +1556,21 @@
"thoughtForFewSeconds": "思考",
"thoughtForSeconds": "思考"
},
"linkSafety": {
"localFileTitle": "開啟本地檔案?",
"externalLinkTitle": "開啟外部連結?",
"localFileDescription": "即將在檔案面板中開啟一個本地檔案。",
"externalLinkDescription": "即將訪問一個外部網站。",
"cancel": "取消",
"opening": "正在開啟…",
"openFile": "開啟檔案",
"openLink": "開啟連結",
"errorCannotOpen": "無法開啟本地檔案",
"errorNoWorkspace": "目前沒有活躍的工作區資料夾。",
"errorOutsideWorkspace": "該檔案不在目前的工作區資料夾內。",
"errorFailedOpen": "開啟本地檔案失敗",
"errorFailedLink": "開啟連結失敗"
},
"messageList": {
"attachedResources": "附加資源",
"loading": "載入中...",