Skip to content

Commit ea0c092

Browse files
authored
Merge branch 'main' into feat/1169-semantic-validation
2 parents 3504bec + 0ef9589 commit ea0c092

41 files changed

Lines changed: 6689 additions & 1339 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@
2525
"axios": "^1.13.1",
2626
"class-variance-authority": "^0.7.1",
2727
"clsx": "^2.1.1",
28+
"i18next": "^26.0.8",
29+
"i18next-browser-languagedetector": "^8.2.1",
2830
"js-yaml": "^4.1.1",
2931
"lucide-react": "^0.548.0",
3032
"motion": "^12.23.24",
3133
"react": "^19.2.0",
3234
"react-dom": "^19.2.0",
3335
"react-error-boundary": "^6.0.0",
36+
"react-i18next": "^17.0.6",
3437
"sonner": "^2.0.7",
3538
"tailwind-merge": "^3.3.1",
3639
"tw-animate-css": "^1.4.0"

app/src/databricks_labs_dqx_app/ui/CLAUDE.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ ui/
3333
│ ├── api.ts # ⚠️ AUTO-GENERATED by orval — types + React Query hooks
3434
│ ├── axios-config.ts # Axios interceptor (error logging)
3535
│ ├── utils.ts # cn() — clsx + tailwind-merge
36-
│ └── selector.ts # Extracts .data from React Query responses
36+
│ ├── selector.ts # Extracts .data from React Query responses
37+
│ └── i18n/ # react-i18next setup + locales/*.json (en, pt-BR, it, es)
3738
├── hooks/
3839
│ └── use-mobile.ts # Mobile viewport detection
3940
├── styles/
@@ -71,6 +72,7 @@ Route tree regenerates automatically when the APX dev server is running (it runs
7172
- **Lucide React** — icons
7273
- **Motion** — animations
7374
- **Sonner** — toast notifications
75+
- **react-i18next** — internationalization (see [Internationalization (i18n)](#internationalization-i18n))
7476
- **js-yaml** — YAML parsing for config editing
7577

7678
## Commands
@@ -124,6 +126,15 @@ npm run preview # Preview production build
124126
- Use `cn()` from `@/lib/utils` for conditional class merging
125127
- Wrap async data components in `Suspense` + error boundaries
126128

129+
### Internationalization (i18n)
130+
131+
The UI is fully localized with **react-i18next**. Locale bundles live in `lib/i18n/locales/*.json`. **Any user-facing string must be translated — never hard-code display text.**
132+
133+
- **Use `t()` for all display text.** Get it from `useTranslation()` (`const { t } = useTranslation()`); reference strings by key (`t("discovery.title")`), never as literals in JSX. This includes `toast` messages, `aria-label`s, placeholders, and error strings.
134+
- **Add every new key to all four locales**`en.json`, `pt-BR.json`, `it.json`, `es.json`. `en.json` is the source of truth. A key present in `en` but missing from the others falls back to English at runtime (a silent partial-translation bug), so keep the key sets in sync and translate the value in each file — don't leave the English string behind in a non-English file.
135+
- **Pluralize with native i18next, not string concatenation.** Use `_one` / `_other` suffix keys with `{{count}}` (e.g. `columnsCount_one` / `columnsCount_other`). Never build plurals with a hard-coded `"s"` suffix or an interpolated `{{somethingPlural}}` placeholder — that bakes English grammar into the translation layer and breaks other locales.
136+
- **Adding a new language:** add it to `SUPPORTED_LANGUAGES` and register a loader in `localeLoaders` (both in `lib/i18n/index.ts`), then create the matching `locales/<code>.json`. Only `en` ships in the initial JS bundle; other locales lazy-load on demand via `ensureLocaleLoaded`, so don't statically import them.
137+
127138
## Vite Config Notes
128139

129140
- **APX Dev Proxy Guard**: Validates `x-apx-dev-proxy` header in dev mode

app/src/databricks_labs_dqx_app/ui/components/AIAssistantProvider.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createContext, useContext, useState, useCallback, type ReactNode } from "react";
22
import { Sparkles } from "lucide-react";
3+
import { useTranslation } from "react-i18next";
34
import { Button } from "@/components/ui/button";
45
import {
56
Sheet,
@@ -32,6 +33,7 @@ export function useAIAssistant() {
3233
}
3334

3435
export function AIAssistantTrigger() {
36+
const { t } = useTranslation();
3537
const { setOpen } = useAIAssistant();
3638
return (
3739
<Button
@@ -41,12 +43,13 @@ export function AIAssistantTrigger() {
4143
className="gap-2"
4244
>
4345
<Sparkles className="h-4 w-4" />
44-
<span className="hidden sm:inline">AI Rules Assistant</span>
46+
<span className="hidden sm:inline">{t("navbar.aiAssistant")}</span>
4547
</Button>
4648
);
4749
}
4850

4951
export function AIAssistantProvider({ children }: { children: ReactNode }) {
52+
const { t } = useTranslation();
5053
const [open, setOpen] = useState(false);
5154
const [isGenerating, setIsGenerating] = useState(false);
5255
const [runContext, setRunContext] = useState<RunContext | null>(null);
@@ -71,9 +74,9 @@ export function AIAssistantProvider({ children }: { children: ReactNode }) {
7174
<Sheet open={open} onOpenChange={setOpen}>
7275
<SheetContent side="right" className="sm:max-w-lg w-[500px] p-0">
7376
<SheetHeader className="sr-only">
74-
<SheetTitle>AI Rules Assistant</SheetTitle>
77+
<SheetTitle>{t("aiAssistant.title")}</SheetTitle>
7578
<SheetDescription>
76-
Generate data quality rules using AI
79+
{t("aiAssistant.description")}
7780
</SheetDescription>
7881
</SheetHeader>
7982
<AICheckGenerator

app/src/databricks_labs_dqx_app/ui/components/AICheckGenerator.tsx

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState } from "react";
2+
import { useTranslation } from "react-i18next";
23
import { Button } from "@/components/ui/button";
34
import { Textarea } from "@/components/ui/textarea";
45
import { Card } from "@/components/ui/card";
@@ -16,30 +17,31 @@ interface AICheckGeneratorProps {
1617
}
1718

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

2325
const handleGenerate = async () => {
2426
if (!userInput.trim()) {
25-
toast.error("Please enter a description of your data quality requirements");
27+
toast.error(t("aiCheckGenerator.enterDescriptionFirst"));
2628
return;
2729
}
2830

2931
try {
3032
const result = await onGenerate(userInput, runContext?.yaml);
3133
setGeneratedYaml(result.yaml_output);
32-
toast.success("Checks generated successfully!");
34+
toast.success(t("aiCheckGenerator.checksGenerated"));
3335
} catch (error) {
34-
toast.error("Failed to generate checks. Please try again.");
36+
toast.error(t("aiCheckGenerator.failedGenerate"));
3537
}
3638
};
3739

3840
const handleCopy = () => {
3941
if (generatedYaml) {
4042
navigator.clipboard.writeText(generatedYaml);
4143
setCopied(true);
42-
toast.success("YAML copied to clipboard");
44+
toast.success(t("aiCheckGenerator.yamlCopied"));
4345
setTimeout(() => setCopied(false), 2000);
4446
}
4547
};
@@ -59,9 +61,9 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
5961
<Sparkles className="h-5 w-5 text-primary" />
6062
</div>
6163
<div className="flex-1 min-w-0">
62-
<h2 className="text-lg font-bold">AI-Assisted Rules Generation</h2>
64+
<h2 className="text-lg font-bold">{t("aiCheckGenerator.title")}</h2>
6365
<p className="text-sm text-muted-foreground">
64-
Describe your data quality needs
66+
{t("aiCheckGenerator.subtitle")}
6567
</p>
6668
</div>
6769
</div>
@@ -71,7 +73,7 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
7173
<div className="mb-3">
7274
<Badge variant="secondary" className="gap-1.5 text-xs font-normal">
7375
<FileCode className="h-3 w-3" />
74-
Using context from: {runContext.runName}
76+
{t("aiCheckGenerator.usingContextFrom", { name: runContext.runName })}
7577
</Badge>
7678
</div>
7779
)}
@@ -90,7 +92,7 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
9092
>
9193
<div className="flex items-center justify-between mb-2">
9294
<h3 className="text-sm font-semibold text-muted-foreground">
93-
Generated Checks
95+
{t("aiCheckGenerator.generatedChecks")}
9496
</h3>
9597
<Button
9698
size="sm"
@@ -101,12 +103,12 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
101103
{copied ? (
102104
<>
103105
<Check className="h-3 w-3" />
104-
Copied
106+
{t("aiCheckGenerator.copied")}
105107
</>
106108
) : (
107109
<>
108110
<Copy className="h-3 w-3" />
109-
Copy
111+
{t("aiCheckGenerator.copy")}
110112
</>
111113
)}
112114
</Button>
@@ -129,11 +131,11 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
129131
<div className="text-center space-y-4 text-muted-foreground">
130132
<Sparkles className="h-16 w-16 mx-auto opacity-20" />
131133
<div>
132-
<p className="font-medium">No rules generated yet</p>
134+
<p className="font-medium">{t("aiCheckGenerator.noRulesYet")}</p>
133135
<p className="text-sm">
134136
{runContext
135-
? `Enter requirements for "${runContext.runName}" to get started`
136-
: "Enter your requirements below to get started"}
137+
? t("aiCheckGenerator.enterRequirementsContext", { name: runContext.runName })
138+
: t("aiCheckGenerator.enterRequirements")}
137139
</p>
138140
</div>
139141
</div>
@@ -151,8 +153,8 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
151153
onKeyDown={handleKeyDown}
152154
placeholder={
153155
runContext
154-
? `Describe rules for "${runContext.runName}"...`
155-
: "Example: Sales amount must be positive"
156+
? t("aiCheckGenerator.describeRulesContext", { name: runContext.runName })
157+
: t("aiCheckGenerator.examplePlaceholder")
156158
}
157159
className="min-h-[100px] resize-none pr-12 bg-card/50 backdrop-blur-sm"
158160
disabled={isGenerating}
@@ -173,8 +175,8 @@ export function AICheckGenerator({ onGenerate, isGenerating, runContext }: AIChe
173175
</div>
174176
</div>
175177
<p className="text-xs text-muted-foreground">
176-
Press <kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">Enter</kbd> to generate or{" "}
177-
<kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">Shift+Enter</kbd> for a new line
178+
<kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">Enter</kbd> {t("aiCheckGenerator.kbdHint")}{" "}
179+
<kbd className="px-1.5 py-0.5 text-xs font-semibold bg-muted rounded">Shift+Enter</kbd> {t("aiCheckGenerator.kbdHintSuffix")}
178180
</p>
179181
</div>
180182
</div>

app/src/databricks_labs_dqx_app/ui/components/AuthGuard.tsx

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { useEffect, useState } from "react";
1+
import { useEffect, useRef, useState } from "react";
22
import axios from "axios";
33
import { Loader2 } from "lucide-react";
4+
import { useTranslation } from "react-i18next";
45
import { currentUser } from "@/lib/api";
56

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

26+
useEffect(() => {
27+
tRef.current = t;
28+
}, [t]);
29+
2330
useEffect(() => {
2431
let cancelled = false;
2532
let timeoutId: NodeJS.Timeout;
@@ -36,7 +43,7 @@ export function AuthGuard({ children }: AuthGuardProps) {
3643

3744
if (retryCount < 15) {
3845
const delay = Math.min(1000 * Math.pow(1.3, retryCount), 3000);
39-
46+
4047
timeoutId = setTimeout(() => {
4148
if (!cancelled) {
4249
setRetryCount((prev) => prev + 1);
@@ -45,14 +52,17 @@ export function AuthGuard({ children }: AuthGuardProps) {
4552
} else {
4653
const errorMessage = axios.isAxiosError(err)
4754
? err.response?.status === 401
48-
? "Authentication timeout. The authentication flow did not complete."
49-
: `Server error (${err.response?.status}): ${err.response?.statusText || err.message}`
55+
? tRef.current("auth.timeoutMessage")
56+
: tRef.current("auth.serverErrorMessage", {
57+
status: err.response?.status ?? "",
58+
statusText: err.response?.statusText || err.message,
59+
})
5060
: err instanceof Error
5161
? err.message
52-
: "Unknown connection error";
53-
62+
: tRef.current("auth.unknownError");
63+
5464
setError(
55-
`${errorMessage}\n\nPlease refresh the page or contact your administrator if the problem persists.`
65+
`${errorMessage}${tRef.current("auth.errorSuffix")}`
5666
);
5767
}
5868
}
@@ -88,14 +98,14 @@ export function AuthGuard({ children }: AuthGuardProps) {
8898
<div className="flex items-center justify-center min-h-screen bg-background">
8999
<div className="text-center space-y-4 p-8 max-w-md">
90100
<div className="text-destructive text-lg font-semibold">
91-
Authentication Error
101+
{t("auth.errorTitle")}
92102
</div>
93103
<p className="text-muted-foreground">{error}</p>
94104
<button
95105
onClick={() => window.location.reload()}
96106
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
97107
>
98-
Refresh Page
108+
{t("auth.refreshPage")}
99109
</button>
100110
</div>
101111
</div>
@@ -108,9 +118,9 @@ export function AuthGuard({ children }: AuthGuardProps) {
108118
<div className="flex items-center justify-center min-h-screen bg-background">
109119
<div className="text-center space-y-4">
110120
<Loader2 className="h-12 w-12 animate-spin text-primary mx-auto" />
111-
<div className="text-lg font-medium">Initializing DQX Studio...</div>
121+
<div className="text-lg font-medium">{t("auth.loadingMessage")}</div>
112122
<p className="text-sm text-muted-foreground">
113-
Setting up your workspace connection
123+
{t("auth.loadingDescription")}
114124
</p>
115125
</div>
116126
</div>

0 commit comments

Comments
 (0)