diff --git a/src/components/ai-elements/link-safety.tsx b/src/components/ai-elements/link-safety.tsx index 02a10c3..c95a738 100644 --- a/src/components/ai-elements/link-safety.tsx +++ b/src/components/ai-elements/link-safety.tsx @@ -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 }) { + 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({ - {isLocalFile ? "Open local file?" : "Open external link?"} + {isLocalFile ? t("localFileTitle") : t("externalLinkTitle")} {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")}
- {url} + {localTarget?.path ?? url}
- Cancel + + {t("cancel")} + - {opening ? "Opening..." : isLocalFile ? "Open file" : "Open link"} + {opening + ? t("opening") + : isLocalFile + ? t("openFile") + : t("openLink")}
@@ -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( diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index ede13da..ed30577 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -1556,6 +1556,21 @@ "thoughtForFewSeconds": "تفكير", "thoughtForSeconds": "تفكير" }, + "linkSafety": { + "localFileTitle": "فتح ملف محلي؟", + "externalLinkTitle": "فتح رابط خارجي؟", + "localFileDescription": "أنت على وشك فتح ملف محلي في لوحة الملفات.", + "externalLinkDescription": "أنت على وشك زيارة موقع ويب خارجي.", + "cancel": "إلغاء", + "opening": "جارٍ الفتح…", + "openFile": "فتح الملف", + "openLink": "فتح الرابط", + "errorCannotOpen": "تعذر فتح الملف المحلي", + "errorNoWorkspace": "لا يوجد مجلد مساحة عمل نشط حالياً.", + "errorOutsideWorkspace": "الملف خارج مجلد مساحة العمل الحالي.", + "errorFailedOpen": "فشل فتح الملف المحلي", + "errorFailedLink": "فشل فتح الرابط" + }, "messageList": { "attachedResources": "الموارد المرفقة", "loading": "جارٍ التحميل...", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 49fdde3..45a7405 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -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...", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index b8ef15f..9a39519 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -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...", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 681c6dc..a664f2d 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -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...", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index b6d5d82..3186a85 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -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...", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 1996d81..048f1df 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -1556,6 +1556,21 @@ "thoughtForFewSeconds": "考えた", "thoughtForSeconds": "考えた" }, + "linkSafety": { + "localFileTitle": "ローカルファイルを開きますか?", + "externalLinkTitle": "外部リンクを開きますか?", + "localFileDescription": "ファイルパネルでローカルファイルを開こうとしています。", + "externalLinkDescription": "外部ウェブサイトにアクセスしようとしています。", + "cancel": "キャンセル", + "opening": "開いています…", + "openFile": "ファイルを開く", + "openLink": "リンクを開く", + "errorCannotOpen": "ローカルファイルを開けません", + "errorNoWorkspace": "現在アクティブなワークスペースフォルダがありません。", + "errorOutsideWorkspace": "ファイルが現在のワークスペースフォルダの外にあります。", + "errorFailedOpen": "ローカルファイルを開けませんでした", + "errorFailedLink": "リンクを開けませんでした" + }, "messageList": { "attachedResources": "添付リソース", "loading": "読み込み中...", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 0a4ef59..5c34ea9 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -1556,6 +1556,21 @@ "thoughtForFewSeconds": "생각함", "thoughtForSeconds": "생각함" }, + "linkSafety": { + "localFileTitle": "로컬 파일을 여시겠습니까?", + "externalLinkTitle": "외부 링크를 여시겠습니까?", + "localFileDescription": "파일 패널에서 로컬 파일을 열려고 합니다.", + "externalLinkDescription": "외부 웹사이트를 방문하려고 합니다.", + "cancel": "취소", + "opening": "열고 있습니다…", + "openFile": "파일 열기", + "openLink": "링크 열기", + "errorCannotOpen": "로컬 파일을 열 수 없습니다", + "errorNoWorkspace": "현재 활성화된 워크스페이스 폴더가 없습니다.", + "errorOutsideWorkspace": "파일이 현재 워크스페이스 폴더 밖에 있습니다.", + "errorFailedOpen": "로컬 파일 열기 실패", + "errorFailedLink": "링크 열기 실패" + }, "messageList": { "attachedResources": "첨부된 리소스", "loading": "불러오는 중...", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 384cec7..3f0cce9 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -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...", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index f727b1a..0f90876 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1556,6 +1556,21 @@ "thoughtForFewSeconds": "思考", "thoughtForSeconds": "思考" }, + "linkSafety": { + "localFileTitle": "打开本地文件?", + "externalLinkTitle": "打开外部链接?", + "localFileDescription": "即将在文件面板中打开一个本地文件。", + "externalLinkDescription": "即将访问一个外部网站。", + "cancel": "取消", + "opening": "正在打开…", + "openFile": "打开文件", + "openLink": "打开链接", + "errorCannotOpen": "无法打开本地文件", + "errorNoWorkspace": "当前没有活跃的工作区文件夹。", + "errorOutsideWorkspace": "该文件不在当前工作区文件夹内。", + "errorFailedOpen": "打开本地文件失败", + "errorFailedLink": "打开链接失败" + }, "messageList": { "attachedResources": "附加资源", "loading": "加载中...", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 88cf58f..6021138 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -1556,6 +1556,21 @@ "thoughtForFewSeconds": "思考", "thoughtForSeconds": "思考" }, + "linkSafety": { + "localFileTitle": "開啟本地檔案?", + "externalLinkTitle": "開啟外部連結?", + "localFileDescription": "即將在檔案面板中開啟一個本地檔案。", + "externalLinkDescription": "即將訪問一個外部網站。", + "cancel": "取消", + "opening": "正在開啟…", + "openFile": "開啟檔案", + "openLink": "開啟連結", + "errorCannotOpen": "無法開啟本地檔案", + "errorNoWorkspace": "目前沒有活躍的工作區資料夾。", + "errorOutsideWorkspace": "該檔案不在目前的工作區資料夾內。", + "errorFailedOpen": "開啟本地檔案失敗", + "errorFailedLink": "開啟連結失敗" + }, "messageList": { "attachedResources": "附加資源", "loading": "載入中...",