Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eda1545
feat: add models settings page with filtering and management
VirenMohindra Dec 20, 2025
069a566
feat: add language filter to models settings page
VirenMohindra Jan 1, 2026
493ecde
feat: add translation consistency checker script
VirenMohindra Jan 6, 2026
198ef05
fix: update imports and add missing translations after rebase
VirenMohindra Jan 17, 2026
ff9764d
fix: translate english placeholders in cs and tr locale files
VirenMohindra Jan 17, 2026
3224eb6
refactor: simplify model dropdown and migrate store to immer
VirenMohindra Jan 28, 2026
a7fbdc7
fix: add download cancellation support and ui improvements
VirenMohindra Jan 28, 2026
a498f40
fix: remove duplicate language/translate settings from general and ad…
VirenMohindra Jan 28, 2026
e15ac6d
fix: prevent model dropdown from being clipped by window edge
VirenMohindra Jan 28, 2026
8adc41c
add languages explicitly, clean up some ui
cjpais Feb 1, 2026
c3c8f54
fix: clear bottom progress bar when download is cancelled
VirenMohindra Feb 1, 2026
a8e0d8a
fix: align model card content to top to prevent floating elements
VirenMohindra Feb 1, 2026
0c2c8de
fix: prevent model deletion from interrupting active extractions
VirenMohindra Feb 1, 2026
c76d875
refactor: migrate ModelCard buttons to Button component
VirenMohindra Feb 1, 2026
99ab6ce
feat: separate downloaded and available models into sections
VirenMohindra Feb 1, 2026
ef6757f
fix: add missing translations after rebase onto main
VirenMohindra Feb 8, 2026
90341ef
fix: add label to model delete button for clearer destructive state
VirenMohindra Feb 8, 2026
337656c
check translations as typescript
cjpais Feb 8, 2026
bd110c1
format
cjpais Feb 8, 2026
957d0c6
better text for dropdown
cjpais Feb 8, 2026
6b02a2c
wip ui tweaks
cjpais Feb 8, 2026
d7c8ffd
rounded + ui tweaks
cjpais Feb 8, 2026
a39db49
fix download not 0% immediately, ui tweaks
cjpais Feb 8, 2026
15f84de
tweak name
cjpais Feb 8, 2026
9dab973
block for model downloading in onboarding
cjpais Feb 8, 2026
34b4358
small fixes
cjpais Feb 8, 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 .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile

- name: Check translation consistency
run: bun run check:translations

- name: Run ESLint
run: bun run lint
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
"format:frontend": "prettier --write .",
"format:backend": "cd src-tauri && cargo fmt",
"test:playwright": "playwright test",
"test:playwright:ui": "playwright test --ui"
"test:playwright:ui": "playwright test --ui",
"check:translations": "bun scripts/check-translations.ts"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
"@tauri-apps/api": "^2.9.0",
"@tauri-apps/plugin-autostart": "~2.5.1",
"@tauri-apps/plugin-clipboard-manager": "~2.3.2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-fs": "~2.4.4",
"@tauri-apps/plugin-global-shortcut": "~2.3.1",
"@tauri-apps/plugin-opener": "^2.5.2",
Expand All @@ -30,16 +32,16 @@
"@tauri-apps/plugin-sql": "~2.3.1",
"@tauri-apps/plugin-store": "~2.4.1",
"@tauri-apps/plugin-updater": "~2.9.0",
"react-select": "^5.8.0",
"tauri-plugin-macos-permissions-api": "2.3.0",
"i18next": "^25.7.2",
"immer": "^11.1.3",
"lucide-react": "^0.542.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^16.4.1",
"react-select": "^5.8.0",
"sonner": "^2.0.7",
"tailwindcss": "^4.1.16",
"tauri-plugin-macos-permissions-api": "2.3.0",
"zod": "^3.25.76",
"zustand": "^5.0.8"
},
Expand Down
224 changes: 224 additions & 0 deletions scripts/check-translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Configuration
const LOCALES_DIR = path.join(__dirname, "..", "src", "i18n", "locales");
const REFERENCE_LANG = "en";

type TranslationData = Record<string, unknown>;

interface ValidationResult {
valid: boolean;
missing: string[][];
extra: string[][];
}

function getLanguages(): string[] {
const entries = fs.readdirSync(LOCALES_DIR, { withFileTypes: true });
return entries
.filter((entry) => entry.isDirectory() && entry.name !== REFERENCE_LANG)
.map((entry) => entry.name)
.sort();
}

const LANGUAGES = getLanguages();

// Colors for terminal output
const colors: Record<string, string> = {
reset: "\x1b[0m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
};

function colorize(text: string, color: string): string {
return `${colors[color]}${text}${colors.reset}`;
}

function getAllKeyPaths(
obj: TranslationData,
prefix: string[] = [],
): string[][] {
let paths: string[][] = [];
for (const key in obj) {
if (!Object.hasOwn(obj, key)) continue;

const currentPath = prefix.concat([key]);
const value = obj[key];

if (typeof value === "object" && value !== null && !Array.isArray(value)) {
paths = paths.concat(
getAllKeyPaths(value as TranslationData, currentPath),
);
} else {
paths.push(currentPath);
}
}
return paths;
}

function hasKeyPath(obj: TranslationData, keyPath: string[]): boolean {
let current: unknown = obj;
for (const key of keyPath) {
if (
typeof current !== "object" ||
current === null ||
(current as Record<string, unknown>)[key] === undefined
) {
return false;
}
current = (current as Record<string, unknown>)[key];
}
return true;
}

function loadTranslationFile(lang: string): TranslationData | null {
const filePath = path.join(LOCALES_DIR, lang, "translation.json");

try {
const content = fs.readFileSync(filePath, "utf8");
return JSON.parse(content) as TranslationData;
} catch (error) {
console.error(colorize(`✗ Error loading ${lang}/translation.json:`, "red"));
console.error(` ${(error as Error).message}`);
return null;
}
}

function validateTranslations(): void {
console.log(colorize("\n🌍 Translation Consistency Check\n", "blue"));

// Load reference file
console.log(`Loading reference language: ${REFERENCE_LANG}`);
const referenceData = loadTranslationFile(REFERENCE_LANG);

if (!referenceData) {
console.error(
colorize(`\n✗ Failed to load reference file (${REFERENCE_LANG})`, "red"),
);
process.exit(1);
}

// Get all key paths from reference
const referenceKeyPaths = getAllKeyPaths(referenceData);
console.log(`Reference has ${referenceKeyPaths.length} keys\n`);

// Track validation results
let hasErrors = false;
const results: Record<string, ValidationResult> = {};

// Validate each language
for (const lang of LANGUAGES) {
const langData = loadTranslationFile(lang);

if (!langData) {
hasErrors = true;
results[lang] = { valid: false, missing: [], extra: [] };
continue;
}

// Find missing keys
const missing = referenceKeyPaths.filter(
(keyPath) => !hasKeyPath(langData, keyPath),
);

// Find extra keys (keys in language but not in reference)
const langKeyPaths = getAllKeyPaths(langData);
const extra = langKeyPaths.filter(
(keyPath) => !hasKeyPath(referenceData, keyPath),
);

results[lang] = {
valid: missing.length === 0 && extra.length === 0,
missing,
extra,
};

if (missing.length > 0 || extra.length > 0) {
hasErrors = true;
}
}

// Print results
console.log(colorize("Results:", "blue"));
console.log("─".repeat(60));

for (const lang of LANGUAGES) {
const result = results[lang];

if (result.valid) {
console.log(
colorize(`✓ ${lang.toUpperCase()}: All keys present`, "green"),
);
} else {
console.log(colorize(`✗ ${lang.toUpperCase()}: Issues found`, "red"));

if (result.missing.length > 0) {
console.log(
colorize(` Missing ${result.missing.length} keys:`, "yellow"),
);
result.missing.slice(0, 10).forEach((keyPath) => {
console.log(` - ${keyPath.join(".")}`);
});
if (result.missing.length > 10) {
console.log(
colorize(
` ... and ${result.missing.length - 10} more`,
"yellow",
),
);
}
}

if (result.extra.length > 0) {
console.log(
colorize(
` Extra ${result.extra.length} keys (not in reference):`,
"yellow",
),
);
result.extra.slice(0, 10).forEach((keyPath) => {
console.log(` - ${keyPath.join(".")}`);
});
if (result.extra.length > 10) {
console.log(
colorize(` ... and ${result.extra.length - 10} more`, "yellow"),
);
}
}

console.log("");
}
}

console.log("─".repeat(60));

// Summary
const validCount = Object.values(results).filter((r) => r.valid).length;
const totalCount = LANGUAGES.length;

if (hasErrors) {
console.log(
colorize(
`\n✗ Validation failed: ${validCount}/${totalCount} languages passed`,
"red",
),
);
process.exit(1);
} else {
console.log(
colorize(
`\n✓ All ${totalCount} languages have complete translations!`,
"green",
),
);
process.exit(0);
}
}

// Run validation
validateTranslations();
Loading