Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
efc1783
Add Switch language to the user's profile
lfbraz May 8, 2026
67add8a
Add yarn.lock
lfbraz May 11, 2026
797d653
Add switch language feature to the doc
lfbraz May 11, 2026
390b648
Merge branch 'main' into feat/profile_switch_language
ghanse May 21, 2026
d361a4c
Update app/src/databricks_labs_dqx_app/ui/lib/i18n/index.ts
lfbraz May 22, 2026
0eb4b45
Remove redundant line in the LanguageSelector
lfbraz May 22, 2026
e1a1602
Fix the i18n anti-pattern
lfbraz May 22, 2026
59025ce
Fix unstable -t in AuthGuard polling-effect deps — read t via useRef
lfbraz May 22, 2026
941dca5
Fall back by primary language tag
lfbraz May 22, 2026
a935c1a
i18n lazy-load non-default locale bundles
lfbraz May 22, 2026
450d692
drop 52 unreferenced common.* keys from all locales
lfbraz May 22, 2026
ea7e06b
Fix the issue: Init failure produces a blank page.
lfbraz Jun 3, 2026
0a4521d
Normalize region-tagged locales without breaking pt-BR
lfbraz Jun 3, 2026
da09af7
Fix LanguageSelector a11y: label hidden trigger, unique id
lfbraz Jun 5, 2026
a0a9093
Handle failed language switch in LanguageSelector
lfbraz Jun 5, 2026
7c0204f
Translate untranslated Discovery strings in it.json
lfbraz Jun 5, 2026
fd8ff96
Document i18n authoring rules in frontend CLAUDE.md
lfbraz Jun 5, 2026
76aaf29
Merge branch 'main' into feat/profile_switch_language
berrybluecode Jun 7, 2026
9830447
Fix hard-coded plural 's' in rulesActive i18n strings
mwojtyczka Jun 8, 2026
0404527
Add CI check for i18n locale key/placeholder parity
mwojtyczka Jun 8, 2026
d638396
Merge branch 'main' into feat/profile_switch_language
mwojtyczka Jun 9, 2026
80f1eaa
fix(app): remove unused formatLabel import in runs.tsx
mwojtyczka Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@
"axios": "^1.13.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"i18next": "^26.0.8",
"i18next-browser-languagedetector": "^8.2.1",
"js-yaml": "^4.1.1",
"lucide-react": "^0.548.0",
"motion": "^12.23.24",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-error-boundary": "^6.0.0",
"react-i18next": "^17.0.6",
Comment thread
mwojtyczka marked this conversation as resolved.
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.4.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
import { Sparkles } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
Sheet,
Expand Down Expand Up @@ -32,6 +33,7 @@ export function useAIAssistant() {
}

export function AIAssistantTrigger() {
const { t } = useTranslation();
const { setOpen } = useAIAssistant();
return (
<Button
Expand All @@ -41,12 +43,13 @@ export function AIAssistantTrigger() {
className="gap-2"
>
<Sparkles className="h-4 w-4" />
<span className="hidden sm:inline">AI Rules Assistant</span>
<span className="hidden sm:inline">{t("navbar.aiAssistant")}</span>
</Button>
);
}

export function AIAssistantProvider({ children }: { children: ReactNode }) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [runContext, setRunContext] = useState<RunContext | null>(null);
Expand All @@ -71,9 +74,9 @@ export function AIAssistantProvider({ children }: { children: ReactNode }) {
<Sheet open={open} onOpenChange={setOpen}>
<SheetContent side="right" className="sm:max-w-lg w-[500px] p-0">
<SheetHeader className="sr-only">
<SheetTitle>AI Rules Assistant</SheetTitle>
<SheetTitle>{t("aiAssistant.title")}</SheetTitle>
<SheetDescription>
Generate data quality rules using AI
{t("aiAssistant.description")}
</SheetDescription>
</SheetHeader>
<AICheckGenerator
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Card } from "@/components/ui/card";
Expand All @@ -16,30 +17,31 @@ interface AICheckGeneratorProps {
}

export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AICheckGeneratorProps) {
const { t } = useTranslation();
const [userInput, setUserInput] = useState("");
const [generatedYaml, setGeneratedYaml] = useState<string | null>(null);
const [copied, setCopied] = useState(false);

const handleGenerate = async () => {
if (!userInput.trim()) {
toast.error("Please enter a description of your data quality requirements");
toast.error(t("aiCheckGenerator.enterDescriptionFirst"));
return;
}

try {
const result = await onGenerate(userInput, runContext?.yaml);
setGeneratedYaml(result.yaml_output);
toast.success("Checks generated successfully!");
toast.success(t("aiCheckGenerator.checksGenerated"));
} catch (error) {
toast.error("Failed to generate checks. Please try again.");
toast.error(t("aiCheckGenerator.failedGenerate"));
}
};

const handleCopy = () => {
if (generatedYaml) {
navigator.clipboard.writeText(generatedYaml);
setCopied(true);
toast.success("YAML copied to clipboard");
toast.success(t("aiCheckGenerator.yamlCopied"));
setTimeout(() => setCopied(false), 2000);
}
};
Expand All @@ -59,9 +61,9 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
<Sparkles className="h-5 w-5 text-primary" />
</div>
<div className="flex-1 min-w-0">
<h2 className="text-lg font-bold">AI-Assisted Rules Generation</h2>
<h2 className="text-lg font-bold">{t("aiCheckGenerator.title")}</h2>
<p className="text-sm text-muted-foreground">
Describe your data quality needs
{t("aiCheckGenerator.subtitle")}
</p>
</div>
</div>
Expand All @@ -71,7 +73,7 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
<div className="mb-3">
<Badge variant="secondary" className="gap-1.5 text-xs font-normal">
<FileCode className="h-3 w-3" />
Using context from: {runContext.runName}
{t("aiCheckGenerator.usingContextFrom", { name: runContext.runName })}
</Badge>
</div>
)}
Expand All @@ -90,7 +92,7 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
>
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-semibold text-muted-foreground">
Generated Checks
{t("aiCheckGenerator.generatedChecks")}
</h3>
<Button
size="sm"
Expand All @@ -101,12 +103,12 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
{copied ? (
<>
<Check className="h-3 w-3" />
Copied
{t("aiCheckGenerator.copied")}
</>
) : (
<>
<Copy className="h-3 w-3" />
Copy
{t("aiCheckGenerator.copy")}
</>
)}
</Button>
Expand All @@ -129,11 +131,11 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
<div className="text-center space-y-4 text-muted-foreground">
<Sparkles className="h-16 w-16 mx-auto opacity-20" />
<div>
<p className="font-medium">No rules generated yet</p>
<p className="font-medium">{t("aiCheckGenerator.noRulesYet")}</p>
<p className="text-sm">
{runContext
? `Enter requirements for "${runContext.runName}" to get started`
: "Enter your requirements below to get started"}
? t("aiCheckGenerator.enterRequirementsContext", { name: runContext.runName })
: t("aiCheckGenerator.enterRequirements")}
</p>
</div>
</div>
Expand All @@ -151,8 +153,8 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
onKeyDown={handleKeyDown}
placeholder={
runContext
? `Describe rules for "${runContext.runName}"...`
: "Example: Sales amount must be positive"
? t("aiCheckGenerator.describeRulesContext", { name: runContext.runName })
: t("aiCheckGenerator.examplePlaceholder")
}
className="min-h-[100px] resize-none pr-12 bg-card/50 backdrop-blur-sm"
disabled={isGenerating}
Expand All @@ -173,8 +175,8 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
</div>
</div>
<p className="text-xs text-muted-foreground">
Press <kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">Enter</kbd> to generate or{" "}
<kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">Shift+Enter</kbd> for a new line
<kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">Enter</kbd> {t("aiCheckGenerator.kbdHint")}{" "}
<kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">Shift+Enter</kbd> {t("aiCheckGenerator.kbdHintSuffix")}
</p>
</div>
</div>
Expand Down
32 changes: 21 additions & 11 deletions app/src/databricks_labs_dqx_app/ui/components/AuthGuard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import axios from "axios";
import { Loader2 } from "lucide-react";
import { useTranslation } from "react-i18next";
import { currentUser } from "@/lib/api";

interface AuthGuardProps {
Expand All @@ -16,10 +17,16 @@ interface AuthGuardProps {
* with the backend OpenAPI spec.
*/
export function AuthGuard({ children }: AuthGuardProps) {
const { t } = useTranslation();
const tRef = useRef(t);
const [isAuthReady, setIsAuthReady] = useState(false);
const [retryCount, setRetryCount] = useState(0);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
tRef.current = t;
}, [t]);

useEffect(() => {
let cancelled = false;
let timeoutId: NodeJS.Timeout;
Expand All @@ -36,7 +43,7 @@ export function AuthGuard({ children }: AuthGuardProps) {

if (retryCount < 15) {
const delay = Math.min(1000 * Math.pow(1.3, retryCount), 3000);

timeoutId = setTimeout(() => {
if (!cancelled) {
setRetryCount((prev) => prev + 1);
Expand All @@ -45,14 +52,17 @@ export function AuthGuard({ children }: AuthGuardProps) {
} else {
const errorMessage = axios.isAxiosError(err)
? err.response?.status === 401
? "Authentication timeout. The authentication flow did not complete."
: `Server error (${err.response?.status}): ${err.response?.statusText || err.message}`
? tRef.current("auth.timeoutMessage")
: tRef.current("auth.serverErrorMessage", {
status: err.response?.status ?? "",
statusText: err.response?.statusText || err.message,
})
: err instanceof Error
? err.message
: "Unknown connection error";
: tRef.current("auth.unknownError");

setError(
`${errorMessage}\n\nPlease refresh the page or contact your administrator if the problem persists.`
`${errorMessage}${tRef.current("auth.errorSuffix")}`
);
}
}
Expand Down Expand Up @@ -88,14 +98,14 @@ export function AuthGuard({ children }: AuthGuardProps) {
<div className="flex items-center justify-center min-h-screen bg-background">
<div className="text-center space-y-4 p-8 max-w-md">
<div className="text-destructive text-lg font-semibold">
Authentication Error
{t("auth.errorTitle")}
</div>
<p className="text-muted-foreground">{error}</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
Refresh Page
{t("auth.refreshPage")}
</button>
</div>
</div>
Expand All @@ -108,9 +118,9 @@ export function AuthGuard({ children }: AuthGuardProps) {
<div className="flex items-center justify-center min-h-screen bg-background">
<div className="text-center space-y-4">
<Loader2 className="h-12 w-12 animate-spin text-primary mx-auto" />
<div className="text-lg font-medium">Initializing DQX Studio...</div>
<div className="text-lg font-medium">{t("auth.loadingMessage")}</div>
<p className="text-sm text-muted-foreground">
Setting up your workspace connection
{t("auth.loadingDescription")}
</p>
</div>
</div>
Expand Down
Loading