Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
115 changes: 89 additions & 26 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -115,40 +115,103 @@
}
</style>
<script>
// Flash prevention: apply theme before CSS loads
(() => {
let stored = null;
try {
stored = localStorage.getItem('windshift-color-mode');
} catch {
// Storage can be unavailable in hardened/private browser contexts.
}
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const mode = stored === 'dark' || (stored === 'system' && prefersDark) || (!stored && prefersDark) ? 'dark' : 'light';
document.documentElement.dataset.colorMode = mode;
// Flash prevention: apply theme before CSS loads.
(() => {
let storedColorMode = null;
let storedLocale = null;
try {
storedColorMode = localStorage.getItem('windshift-color-mode');
storedLocale = localStorage.getItem('windshift-locale');
} catch {
// Storage can be unavailable in hardened/private browser contexts.
}

const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const colorMode =
storedColorMode === 'dark' ||
(storedColorMode === 'system' && prefersDark) ||
(!storedColorMode && prefersDark)
? 'dark'
: 'light';
document.documentElement.dataset.colorMode = colorMode;

const bootTranslations = {
en: {
starting: 'Starting Windshift…',
slow: 'This is taking longer than expected. Check your connection or server.',
retry: 'Retry'
},
de: {
starting: 'Windshift wird gestartet…',
slow: 'Dies dauert länger als erwartet. Überprüfen Sie Ihre Verbindung oder den Server.',
retry: 'Erneut versuchen'
},
es: {
starting: 'Iniciando Windshift…',
slow: 'Esto está tardando más de lo esperado. Comprueba tu conexión o el servidor.',
retry: 'Reintentar'
},
ar: {
starting: 'جارٍ تشغيل Windshift…',
slow: 'يستغرق هذا وقتًا أطول من المتوقع. تحقق من اتصالك أو الخادم.',
retry: 'إعادة المحاولة'
},
'pt-BR': {
starting: 'Iniciando o Windshift…',
slow: 'Isso está demorando mais do que o esperado. Verifique sua conexão ou o servidor.',
retry: 'Tentar novamente'
},
ru: {
starting: 'Запуск Windshift…',
slow: 'Это занимает больше времени, чем ожидалось. Проверьте подключение или сервер.',
retry: 'Повторить'
},
'zh-CN': {
starting: '正在启动 Windshift…',
slow: '所需时间超出预期。请检查网络连接或服务器。',
retry: '重试'
}
};
const supportedLocales = Object.keys(bootTranslations);
const browserLocale = navigator.language || 'en';
const locale =
(storedLocale && supportedLocales.includes(storedLocale) && storedLocale) ||
supportedLocales.find((candidate) => candidate.toLowerCase() === browserLocale.toLowerCase()) ||
supportedLocales.find(
(candidate) => candidate.split('-')[0] === browserLocale.toLowerCase().split('-')[0]
) ||
'en';
const messages = bootTranslations[locale];
document.documentElement.lang = locale;
document.documentElement.dir = locale === 'ar' ? 'rtl' : 'ltr';

window.addEventListener('DOMContentLoaded', () => {
const boot = document.getElementById('windshift-boot');
const starting = document.getElementById('windshift-boot-starting');
const retry = document.getElementById('windshift-boot-retry');
const detail = document.getElementById('windshift-boot-detail');
if (!boot || !starting || !retry || !detail) return;

window.addEventListener('DOMContentLoaded', () => {
const boot = document.getElementById('windshift-boot');
const retry = document.getElementById('windshift-boot-retry');
const detail = document.getElementById('windshift-boot-detail');
if (!boot || !retry || !detail) return;
retry.addEventListener('click', () => location.reload());
window.setTimeout(() => {
// main.js removes the placeholder before mounting App.
if (!document.getElementById('windshift-boot')) return;
detail.hidden = false;
retry.hidden = false;
}, 10_000);
});
})();
starting.textContent = messages.starting;
detail.textContent = messages.slow;
retry.textContent = messages.retry;
retry.addEventListener('click', () => location.reload());
window.setTimeout(() => {
// main.js removes the placeholder before mounting App.
if (!document.getElementById('windshift-boot')) return;
detail.hidden = false;
retry.hidden = false;
}, 10_000);
});
})();
</script>
</head>
<body>
<div id="app">
<div id="windshift-boot" role="status" aria-live="polite">
<div>
<img class="boot-logo" src="windshift-3.svg" alt="Windshift" width="64" height="64" />
<p>Starting Windshift…</p>
<p id="windshift-boot-starting">Starting Windshift…</p>
<p id="windshift-boot-detail" class="boot-detail" hidden>
This is taking longer than expected. Check your connection or server.
</p>
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"format:check": "biome format .",
"lint": "biome lint .",
"lint:fix": "biome lint --write .",
"check": "biome check . && node scripts/check-shortcuts.js && node scripts/check-i18n.js",
"check": "biome check . && node scripts/check-shortcuts.js && node scripts/check-i18n.js && node scripts/check-hardcoded-i18n.js",
"check:entry-assets": "node scripts/check-entry-assets.js",
"check:fix": "biome check --write .",
"test": "bun --bun vitest",
Expand Down
107 changes: 107 additions & 0 deletions frontend/scripts/check-hardcoded-i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');

// This list is an incremental ratchet: once a user-facing screen has been
// migrated, literal English UI copy must not return to it.
const guardedFiles = [
'src/lib/layout/DashboardCustomizationSidebar.svelte',
'src/lib/pages/Homepage.svelte',
'src/lib/features/workflows/WorkflowBuilder.svelte',
'src/lib/pages/Screens.svelte',
'src/lib/pickers/ConfigurationSetPicker.svelte',
'src/lib/pickers/ScreenPicker.svelte',
'src/lib/pickers/WorkflowPicker.svelte',
'src/lib/settings/ConfigurationSetManager.svelte',
'src/lib/settings/ConfigurationSetItemTypes.svelte',
'src/lib/settings/ActionCapabilitiesManager.svelte',
'src/lib/settings/ActionCredentialManager.svelte',
'src/lib/settings/AgentTemplateManager.svelte',
'src/lib/settings/CapabilityManager.svelte',
'src/lib/settings/HierarchyLevelManager.svelte',
'src/lib/settings/ItemTypeManager.svelte',
'src/lib/settings/IntegrationProviderManager.svelte',
'src/lib/settings/IntegrationsManager.svelte',
'src/lib/settings/LLMConnectionManager.svelte',
'src/lib/settings/LinkTypeManager.svelte',
'src/lib/settings/OAuthClientManager.svelte',
'src/lib/settings/PriorityManager.svelte',
'src/lib/settings/StatusCategoryManager.svelte',
'src/lib/settings/StatusManager.svelte',
'src/lib/settings/ThemeManager.svelte',
'src/lib/settings/RunnerPoolManager.svelte',
'src/lib/workspaces/WorkspaceConfigurationAssigner.svelte',
'src/lib/workspaces/WorkspaceConfigurationPreview.svelte',
'src/lib/workspaces/WorkspaceMembers.svelte',
'src/lib/workspaces/WorkspaceCustomizationSidebar.svelte',
'src/lib/workspaces/WorkspaceNavigation.svelte',
'src/lib/workspaces/WorkspaceWelcome.svelte',
'src/lib/workspaces/Workspaces.svelte',
'src/lib/widgets/CompletionChartWidget.svelte',
'src/lib/widgets/CreatedChartWidget.svelte',
'src/lib/widgets/IterationTimelineWidget.svelte',
'src/lib/widgets/MilestoneProgressWidget.svelte',
'src/lib/widgets/MyTasksWidget.svelte',
'src/lib/widgets/OverdueItemsWidget.svelte',
'src/lib/widgets/RecentItemsWidget.svelte',
'src/lib/widgets/StatsCardWidget.svelte',
'src/lib/widgets/TestCoverageWidget.svelte',
'src/lib/widgets/UpcomingDeadlinesWidget.svelte',
'src/lib/widgets/WidgetState.svelte',
'src/lib/widgets/WidgetWrapper.svelte',
'src/lib/widgets/dashboard/AssignedToMeWidget.svelte',
'src/lib/widgets/dashboard/DailyBriefingWidget.svelte',
'src/lib/widgets/dashboard/DashboardItemRow.svelte',
'src/lib/widgets/dashboard/DashboardTaskList.svelte',
'src/lib/widgets/dashboard/DueMark.svelte',
'src/lib/widgets/dashboard/PersonalTasksWidget.svelte',
'src/lib/widgets/dashboard/QuickAccessWidget.svelte',
'src/lib/widgets/dashboard/RecentWorkspacesWidget.svelte',
'src/lib/widgets/dashboard/SavedSearchWidget.svelte',
'src/lib/widgets/dashboard/UpcomingMilestonesWidget.svelte',
'src/lib/widgets/dashboard/WatchedItemsWidget.svelte',
'src/lib/widgets/dashboard/WhatsNewWidget.svelte',
'src/lib/widgets/dashboard/YourActivityWidget.svelte',
];

const rules = [
{
name: 'visible text',
pattern: />\s*([A-Z][^<{\n]*?)\s*</g,
},
{
name: 'localizable attribute',
pattern:
/(?:placeholder|title|aria-label|emptyMessage|emptyDescription|confirmLabel|cancelLabel|subtitle)=["']([A-Z][^"']+)["']/g,
},
];

const intentionalLiterals = new Set(['Esc']);

const violations = [];

for (const relativeFile of guardedFiles) {
// Script blocks contain operators such as `=>` that look like HTML text to
// the lightweight regex guard. UI copy lives in the component markup.
const source = readFileSync(path.join(root, relativeFile), 'utf8').replace(
/<script\b[^>]*>[\s\S]*?<\/script>/g,
(script) => '\n'.repeat(script.split('\n').length - 1)
);
for (const rule of rules) {
for (const match of source.matchAll(rule.pattern)) {
if (intentionalLiterals.has(match[1].trim())) continue;
const line = source.slice(0, match.index).split('\n').length;
violations.push(`${relativeFile}:${line} ${rule.name}: ${JSON.stringify(match[1].trim())}`);
}
}
}

if (violations.length > 0) {
console.error('Hardcoded i18n guard failed. Move this copy into the locale catalog:');
for (const violation of violations) console.error(` ${violation}`);
process.exit(1);
}

console.log(`Hardcoded i18n guard passed (${guardedFiles.length} migrated screens).`);
Loading
Loading