From d5db5fd58d51a71955f7cfafdd0054d5d0d4a368 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 18 Jul 2026 12:32:07 -0700 Subject: [PATCH] FN-8304: detect foreign issue-form content for translation Recognize foreign GitHub and GitLab issue-form prose so import translation remains available. - Remove issue-form scaffolding before content-language scoring. - Identify Czech as unsupported foreign Latin content and offer translation. - Cover automatic and manual translation paths with issue-form fixtures. - Document the expanded import translation behavior and add a patch changeset. Files changed: .changeset/fn-8304-issue-form-translation.md | 7 ++ docs/dashboard-guide.md | 2 +- docs/settings-reference.md | 2 +- packages/core/src/detect-content-language.ts | 66 ++++++++++-- .../__tests__/GitHubImportAutoTranslate.test.tsx | 42 +++++++- .../utils/__tests__/detectContentLanguage.test.ts | 120 +++++++++++++++++++++ .../src/__tests__/import-translate-service.test.ts | 56 ++++++++++ 7 files changed, 284 insertions(+), 11 deletions(-) Fusion-Task-Id: FN-8304 Fusion-Task-Lineage: ef6ca70c-aa67-40ff-a23a-077940b5a5f0 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8304-issue-form-translation.md | 7 + docs/dashboard-guide.md | 2 +- docs/settings-reference.md | 2 +- packages/core/src/detect-content-language.ts | 66 ++++++++-- .../GitHubImportAutoTranslate.test.tsx | 42 +++++- .../__tests__/detectContentLanguage.test.ts | 120 ++++++++++++++++++ .../import-translate-service.test.ts | 56 ++++++++ 7 files changed, 284 insertions(+), 11 deletions(-) create mode 100644 .changeset/fn-8304-issue-form-translation.md diff --git a/.changeset/fn-8304-issue-form-translation.md b/.changeset/fn-8304-issue-form-translation.md new file mode 100644 index 0000000000..6e604674fb --- /dev/null +++ b/.changeset/fn-8304-issue-form-translation.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Foreign-language GitHub/GitLab issues authored via issue forms now auto-translate and offer the Translate button. +category: fix +dev: detectContentLanguage now strips issue-form scaffolding line-by-line (headers, standalone bold field-label lines, checkboxes, `_No response_`, HTML comments) and strips only the leading `>` blockquote marker while retaining quoted content, before script/stopword scoring, so form bodies are no longer scored as English and skipped by both the server auto-translate eligibility (isTranslatable) and the client offer path. Stripping is line-scoped so inline bold/list/quote content in ordinary prose (triage/ai-summary inputs) is preserved. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 37d9151424..dae152bc48 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -337,7 +337,7 @@ Use Import Tasks on desktop/tablet: 3. Stay on **Issues** or switch to **Pull Requests**, then optionally enter issue label filters before loading results. Expected outcome: the list pane shows matching open issues or pull requests and marks entries that already exist on the board. Use **Hide imported** beside the imported count to remove those unavailable rows from the current Issues, Pull Requests, or GitLab list; turning it off restores the greyed **Imported** rows. After a successful GitHub or GitLab import, the source row is marked **Imported** and made unavailable immediately, without waiting for the board list to refresh. 4. Select an issue or pull request row. - Expected outcome: the full-width candidate list stays visible while its title, source link, body, labels or PR metadata, and import controls open in a draggable and resizable detail window. On mobile, that detail is a full-screen sheet. When selected title/body content is in another language, the detail offers **Translate**, **Show original** / **Show translation**, and **Dismiss**; translation is display-only. With GitHub import auto-translate enabled, every reachable page of open GitHub issues is translated as you page through the fetched list (up to the 300-issue fetch cap per one-hour translate budget); repeat views use the translation cache. While a page is translating, the issues list shows a **Translating…** status indicator and surfaces any translation failure without blocking import or browsing. Pull request and GitLab lists retain the per-selection translation flow. A pull request preview also shows its checks; use **Refresh checks** to fetch current GitHub check status and comments without reopening the detail. Each failed check has a **Create fix task** action that creates a new task prefilled with the repository, PR, branches, check status, and check-details link. + Expected outcome: the full-width candidate list stays visible while its title, source link, body, labels or PR metadata, and import controls open in a draggable and resizable detail window. On mobile, that detail is a full-screen sheet. When selected title/body content is in another language, the detail offers **Translate**, **Show original** / **Show translation**, and **Dismiss**; translation is display-only. Issue forms are evaluated from their user-provided answers rather than template headings, labels, placeholders, and checkboxes, so foreign-language form content receives the same translation offer. With GitHub import auto-translate enabled, every reachable page of open GitHub issues is translated as you page through the fetched list (up to the 300-issue fetch cap per one-hour translate budget); repeat views use the translation cache. While a page is translating, the issues list shows a **Translating…** status indicator and surfaces any translation failure without blocking import or browsing. Pull request and GitLab lists retain the per-selection translation flow. A pull request preview also shows its checks; use **Refresh checks** to fetch current GitHub check status and comments without reopening the detail. Each failed check has a **Create fix task** action that creates a new task prefilled with the repository, PR, branches, check status, and check-details link. /g, " ") .replace(/```[\s\S]*?```/g, " ") .replace(/`[^`]+`/g, " ") .replace(/https?:\/\/\S+/gi, " ") .replace(/@[\w-]+/g, " ") - .replace(/#\d+/g, " "); + .replace(/#\d+/g, " ") + .split(/\r?\n/) + .flatMap((line) => { + // Quoted user prose is content; do not apply form-scaffold filtering to it. + if (/^\s*>\s?/.test(line)) return [line.replace(/^\s*>\s?/, "")]; + if (hasIssueFormScaffolding && /^\s*#{1,6}\s+.*$/.test(line)) return []; + if (hasIssueFormScaffolding && /^\s*\*\*[^*\n]+\*\*\s*:?\s*$/.test(line)) return []; + if (hasIssueFormScaffolding && /^\s*[-*]\s*\[[ xX]\]\s*.*$/.test(line)) return []; + if (hasIssueFormScaffolding && /^\s*_No response_\s*$/i.test(line)) return []; + return [line]; + }) + .join("\n"); const hangul = countMatches(cleaned, /[\uAC00-\uD7AF]/g); const hiraganaKatakana = countMatches(cleaned, /[\u3040-\u30FF]/g); @@ -185,6 +225,16 @@ export function contentNeedsTranslation( if (detected.confidence === "high" && detected.family !== familyForLocale(dashboardLocale) && detected.family !== "other") { return { needed: true, detected }; } + /* + FNXC:GitHubImportTranslate 2026-07-18-19:05: + An unsupported Latin language with a high-confidence stopword match (for example Czech in + issue #2306) has no safe supported locale label, but it is foreign to every supported dashboard + Latin locale. Offer translation rather than treating `unknown` as English and skipping both + import auto-translation and the preview CTA. + */ + if (detected.confidence === "high" && detected.family === "latin") { + return { needed: true, detected }; + } return { needed: false, detected }; } diff --git a/packages/dashboard/app/components/__tests__/GitHubImportAutoTranslate.test.tsx b/packages/dashboard/app/components/__tests__/GitHubImportAutoTranslate.test.tsx index aa4b80e264..06c5f7f4bc 100644 --- a/packages/dashboard/app/components/__tests__/GitHubImportAutoTranslate.test.tsx +++ b/packages/dashboard/app/components/__tests__/GitHubImportAutoTranslate.test.tsx @@ -7,7 +7,7 @@ awaits the run. These pin that contract — a regression to a single all-or-noth as "no translations until every issue finishes", which is exactly what these detect. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { renderHook, waitFor, cleanup } from "@testing-library/react"; +import { render, renderHook, waitFor, cleanup } from "@testing-library/react"; const { autoTranslateImportIssues } = vi.hoisted(() => ({ autoTranslateImportIssues: vi.fn() })); @@ -28,6 +28,7 @@ vi.mock("react-i18next", () => ({ import { useGitHubImportAutoTranslate, + useGitHubImportTranslation, AUTO_TRANSLATE_CHUNK_SIZE, hashImportItemsForKey, } from "../GitHubImportTranslateControls"; @@ -67,6 +68,29 @@ afterEach(() => { const base = { enabled: true, owner: "o", repo: "r", reloadGeneration: 0, targetLocale: "en" as const }; +const REPORTED_CZECH_ISSUE_FORM = ` +# PWA na iOS: studený start bez tokenu skončí ve smyčce „Can't reach Fusion Backend" — dialog pro vložení tokenu se nikdy nezobrazí + +**GitHub issue:** Runfusion/Fusion#TBD +**Verze Fusion:** 0.72.0 (zdrojový checkout) +**Oblast:** Dashboard / autentizace (PWA, vzdálený přístup přes tunel) +**Závažnost:** Vysoká — aplikaci přidanou na plochu iPhonu nelze vůbec autorizovat. + +## Shrnutí + +Dashboard zpřístupněný přes Remote Access funguje v mobilním Safari správně. Token přiteče přes přihlašovací URL a uloží se do localStorage. Po přidání na plochu na iOS ale instalovaná webová aplikace startuje bez tokenu v URL, běží v izolovaném úložišti a místo dialogu pro vložení tokenu zobrazí jen chybovou stránku Unauthorized. + +## Reprodukce + +1. Spusťte dashboard s aktivní bearer-token autentizací a zpřístupněte ho přes tunel. +2. Na iPhonu otevřete přihlašovací URL v Safari a potom aplikaci přidejte na plochu. +3. Otevřete aplikaci z plochy: zobrazí se chyba Unauthorized a tlačítko Retry Connection nic nedělá. + +## Očekávané chování + +Nepřihlášený studený start nabídne vložení tokenu, aby aplikace nebyla trvale nepoužitelná. +`; + describe("useGitHubImportAutoTranslate — background streaming", () => { it("returns immediately with no translations (the list never waits on it)", () => { autoTranslateImportIssues.mockReturnValue(new Promise(() => {})); // never resolves @@ -277,6 +301,22 @@ Regression: PR #2147 review. The signature delimiter was ambiguous because prose moving a `|` between title and body produced an unchanged signature and the panel kept serving the OLD translation. Length-prefixing makes the encoding injective. */ +describe("useGitHubImportTranslation — manual offer", () => { + it("renders the translate offer for a foreign issue-form body when auto-translate is off", () => { + const { result } = renderHook(() => useGitHubImportTranslation({ + selectionKey: "issue:2306", + title: "PWA na iOS bez tokenu", + body: REPORTED_CZECH_ISSUE_FORM, + dashboardLocale: "en", + autoTranslateEnabled: false, + })); + + const view = render(result.current.controls); + expect(view.getByTestId("github-import-translate-message").textContent).toContain("This content appears"); + expect(view.getByTestId("github-import-translate-action")).toHaveTextContent("Translate"); + }); +}); + describe("hashImportItemsForKey", () => { it("distinguishes content that only differs in where a delimiter falls", () => { const a = [{ number: 1, title: "a|b", body: "c", state: "open" as const }]; diff --git a/packages/dashboard/app/utils/__tests__/detectContentLanguage.test.ts b/packages/dashboard/app/utils/__tests__/detectContentLanguage.test.ts index 8469d425ed..9b4fce8d82 100644 --- a/packages/dashboard/app/utils/__tests__/detectContentLanguage.test.ts +++ b/packages/dashboard/app/utils/__tests__/detectContentLanguage.test.ts @@ -70,6 +70,75 @@ See https://github.com/owner/repo/issues/1 for context about the users. }); }); +/* Realistic issue-form fixtures deliberately include English scaffolding around user prose. */ +const SPANISH_ISSUE_FORM = ` + +### Bug description +**What happened?** +### Expected behavior +**What did you expect to happen?** +### Environment +**Which operating system and version are you using?** +### Additional details +**What other context would help us investigate?** +El servidor devuelve un error cuando el usuario intenta guardar los cambios y la aplicación no responde. Por favor revise los registros porque este problema ocurre para todos los usuarios después de actualizar la configuración. + +### Steps to reproduce +**Steps** +- [x] I searched existing issues +- [ ] I can provide more details +1. Abra la configuración y guarde los cambios para comprobar que el fallo aparece de nuevo cuando el sistema procesa la solicitud. + +### Additional context +**Logs** +_No response_ +`; + +const HANGUL_ISSUE_FORM = ` + +### Bug description +**What happened?** +### Expected behavior +**What did you expect to happen?** +### Environment +**Which operating system and version are you using?** +### Additional details +**What other context would help us investigate?** +대시보드에서 설정을 저장하면 오류가 발생하고 사용자가 변경한 내용을 확인할 수 없습니다. 이 문제는 모든 프로젝트에서 반복되며 화면을 새로 고쳐도 계속됩니다. + +### Steps to reproduce +**Steps** +- [x] I searched existing issues +- [ ] I can provide more details + +### Additional context +**Logs** +_No response_ +`; + +const REPORTED_CZECH_ISSUE_FORM = ` +# PWA na iOS: studený start bez tokenu skončí ve smyčce „Can't reach Fusion Backend" — dialog pro vložení tokenu se nikdy nezobrazí + +**GitHub issue:** Runfusion/Fusion#TBD +**Verze Fusion:** 0.72.0 (zdrojový checkout) +**Oblast:** Dashboard / autentizace (PWA, vzdálený přístup přes tunel) +**Závažnost:** Vysoká — aplikaci přidanou na plochu iPhonu nelze vůbec autorizovat. + +## Shrnutí + +Dashboard zpřístupněný přes Remote Access funguje v mobilním Safari správně. Token přiteče přes přihlašovací URL a uloží se do localStorage. Po přidání na plochu na iOS ale instalovaná webová aplikace startuje bez tokenu v URL, běží v izolovaném úložišti a místo dialogu pro vložení tokenu zobrazí jen chybovou stránku Unauthorized. + +## Reprodukce + +1. Spusťte dashboard s aktivní bearer-token autentizací a zpřístupněte ho přes tunel. +2. Na iPhonu otevřete přihlašovací URL v Safari a potom aplikaci přidejte na plochu. +3. Otevřete aplikaci z plochy: zobrazí se chyba Unauthorized a tlačítko Retry Connection nic nedělá. + +## Očekávané chování + +Nepřihlášený studený start nabídne vložení tokenu, aby aplikace nebyla trvale nepoužitelná. +`; + describe("contentNeedsTranslation", () => { const french = "Cette issue décrit le problème avec l'aperçu d'importation et ce que nous devrions changer pour les utilisateurs qui ont du contenu dans une autre langue dans le tableau de bord."; @@ -100,6 +169,57 @@ describe("contentNeedsTranslation", () => { it("offers translation for Chinese content when dashboard is English", () => { expect(contentNeedsTranslation(chinese, "en").needed).toBe(true); }); + + it("detects foreign GitHub issue-form answers instead of English scaffolding", () => { + expect(contentNeedsTranslation(SPANISH_ISSUE_FORM, "en").needed).toBe(true); + expect(contentNeedsTranslation(HANGUL_ISSUE_FORM, "en").needed).toBe(true); + }); + + it("recognizes the reported Czech issue-form body as unsupported foreign Latin prose", () => { + const result = contentNeedsTranslation(REPORTED_CZECH_ISSUE_FORM, "en"); + expect(result.needed).toBe(true); + expect(result.detected).toMatchObject({ locale: "unknown", family: "latin", confidence: "high" }); + }); + + it("does not offer translation for English issue-form answers", () => { + const englishForm = SPANISH_ISSUE_FORM.replace( + /El servidor[\s\S]*?solicitud\./, + "The server returns an error when a user saves settings, and the application stops responding for every project after configuration changes.", + ).replace( + /Abra la configuración[\s\S]*?solicitud\./, + "Open settings and save the changes to confirm that the problem happens again when the system processes the request.", + ); + expect(contentNeedsTranslation(englishForm, "en").needed).toBe(false); + }); + + it("keeps scaffold-only issue forms unknown", () => { + const scaffoldOnly = ` +### Bug description +**What happened?** +_No response_ +- [x] I searched existing issues +`; + const result = contentNeedsTranslation(scaffoldOnly, "en"); + expect(result.needed).toBe(false); + expect(result.detected.locale).toBe("unknown"); + }); + + it("preserves inline bold, list prose, and quoted foreign content", () => { + const ordinaryProse = `**palabra importante** +- falla al guardar +> Esta explicación citada confirma que el servidor devuelve errores para todos los proyectos y los usuarios cuando intentan guardar cambios en la configuración.`; + expect(contentNeedsTranslation("**palabra importante**\n- falla al guardar", "en").needed).toBe(false); + expect(contentNeedsTranslation(ordinaryProse, "en").needed).toBe(true); + }); + + it("preserves meaningful headings and task-list prose outside issue forms", () => { + const markdownReport = `### Informe del problema +- [x] El servidor devuelve errores para todos los usuarios cuando guardan cambios en la configuración. +### Pasos realizados +- [x] Abra el panel, cambie una opción y guarde para confirmar que el fallo continúa.`; + + expect(contentNeedsTranslation(markdownReport, "en").needed).toBe(true); + }); }); describe("localeDisplayName", () => { diff --git a/packages/dashboard/src/__tests__/import-translate-service.test.ts b/packages/dashboard/src/__tests__/import-translate-service.test.ts index dcde8c16c9..b247f284e1 100644 --- a/packages/dashboard/src/__tests__/import-translate-service.test.ts +++ b/packages/dashboard/src/__tests__/import-translate-service.test.ts @@ -86,6 +86,51 @@ const SPANISH_BODY = const ENGLISH_BODY = "The server returns an error when the user tries to save the changes to the configuration. The operation cannot be completed because the system does not respond. Please review the server logs for more information about this problem."; +const REPORTED_CZECH_ISSUE_FORM = ` +# PWA na iOS: studený start bez tokenu skončí ve smyčce „Can't reach Fusion Backend" — dialog pro vložení tokenu se nikdy nezobrazí + +**GitHub issue:** Runfusion/Fusion#TBD +**Verze Fusion:** 0.72.0 (zdrojový checkout) +**Oblast:** Dashboard / autentizace (PWA, vzdálený přístup přes tunel) +**Závažnost:** Vysoká — aplikaci přidanou na plochu iPhonu nelze vůbec autorizovat. + +## Shrnutí + +Dashboard zpřístupněný přes Remote Access funguje v mobilním Safari správně. Token přiteče přes přihlašovací URL a uloží se do localStorage. Po přidání na plochu na iOS ale instalovaná webová aplikace startuje bez tokenu v URL, běží v izolovaném úložišti a místo dialogu pro vložení tokenu zobrazí jen chybovou stránku Unauthorized. + +## Reprodukce + +1. Spusťte dashboard s aktivní bearer-token autentizací a zpřístupněte ho přes tunel. +2. Na iPhonu otevřete přihlašovací URL v Safari a potom aplikaci přidejte na plochu. +3. Otevřete aplikaci z plochy: zobrazí se chyba Unauthorized a tlačítko Retry Connection nic nedělá. + +## Očekávané chování + +Nepřihlášený studený start nabídne vložení tokenu, aby aplikace nebyla trvale nepoužitelná. +`; + +const GITLAB_HANGUL_ISSUE_FORM = ` + +### Bug description +**What happened?** +### Expected behavior +**What did you expect to happen?** +### Environment +**Which operating system and version are you using?** +### Additional details +**What other context would help us investigate?** +대시보드에서 설정을 저장하면 오류가 발생하고 사용자가 변경한 내용을 확인할 수 없습니다. 이 문제는 모든 프로젝트에서 반복되며 화면을 새로 고쳐도 계속됩니다. + +### Steps to reproduce +**Steps** +- [x] I searched existing issues +- [ ] I can provide more details + +### Additional context +**Logs** +_No response_ +`; + beforeEach(() => { translateTextMock.mockReset(); translateTextMock.mockResolvedValue({ title: "TRANSLATED", body: "TRANSLATED BODY" }); @@ -143,6 +188,17 @@ describe("isTranslatable", () => { it("accepts foreign-language open content", () => { expect(isTranslatable({ number: 1, title: "Error del servidor", body: SPANISH_BODY, state: "open" }, "en")).toBe(true); }); + + it("selects foreign GitHub and GitLab issue-form bodies through the provider-agnostic seam", () => { + const items = [ + { number: 2306, title: "PWA na iOS bez tokenu", body: REPORTED_CZECH_ISSUE_FORM, state: "open" as const }, + { number: 2307, title: "설정 저장 오류", body: GITLAB_HANGUL_ISSUE_FORM, state: "open" as const }, + ]; + + expect(isTranslatable(items[0], "en")).toBe(true); + expect(isTranslatable(items[1], "en")).toBe(true); + expect(selectEligibleItems(items, "en").map((item) => item.number)).toEqual([2306, 2307]); + }); }); describe("hashSourceContent", () => {