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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ The built-in MCP server exposes work items, comments, pages, knowledge, tests, t

**Internationalization**

- Ten built-in languages: English, Deutsch, Español, Français, Italiano, Português (Brasil), العربية, Русский, 简体中文, and 한국어, with right-to-left support for Arabic.
- Eleven built-in languages: English, Deutsch, Español, Français, Italiano, Português (Brasil), العربية, فارسی, Русский, 简体中文, and 한국어, with right-to-left support for Arabic and Persian.

</details>

Expand Down Expand Up @@ -175,7 +175,7 @@ The built-in MCP server exposes work items, comments, pages, knowledge, tests, t

<p align="center"><strong>Available in many languages · Disponibile in molte lingue · متوفر بالعديد من اللغات · 支持多种语言<br>Доступно на многих языках · 다양한 언어로 사용 가능</strong></p>

Windshift ships with ten built-in languages: English, Deutsch, Español, Français, Italiano, Português (Brasil), العربية, Русский, 简体中文, and 한국어, including right-to-left support for Arabic.
Windshift ships with eleven built-in languages: English, Deutsch, Español, Français, Italiano, Português (Brasil), العربية, فارسی, Русский, 简体中文, and 한국어, including right-to-left support for Arabic and Persian.

<p align="center">
<picture>
Expand Down
1 change: 1 addition & 0 deletions frontend/package-lock.json

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

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@fontsource/geist-sans": "^5.2.5",
"@fontsource/inter": "^5.2.8",
"@github/hotkey": "^3.1.1",
"@internationalized/date": "^3.12.0",
"@lucide/svelte": "^1.14.0",
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.86.6",
Expand Down
10 changes: 5 additions & 5 deletions frontend/scripts/check-i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
const LOCALES_DIR = join(__dirname, '..', 'src', 'lib', 'locales');
const SRC_DIR = join(__dirname, '..', 'src');
const REFERENCE_LOCALE = 'en';
// Russian, German, French, and Simplified Chinese are shipped as fully reviewed
// locales and must never silently fall back to English. Older locales may remain
// partially translated while still using the application's documented English
// runtime fallback.
const REQUIRED_FULL_COVERAGE_LOCALES = new Set(['ru', 'de', 'fr', 'zh-CN']);
// Russian, German, French, Simplified Chinese, and Persian are shipped with full
// key coverage and must never silently fall back to English. Older locales may
// remain partially translated while still using the application's documented
// English runtime fallback.
const REQUIRED_FULL_COVERAGE_LOCALES = new Set(['ru', 'de', 'fr', 'zh-CN', 'fa']);
const PLURAL_SUFFIX_PATTERN = /_(zero|one|two|few|many|other)$/;

// These values intentionally retain product names, code syntax, URLs, or sample identifiers.
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/design-system/viewer/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard';
import ClipboardPenLine from '@lucide/svelte/icons/clipboard-pen-line';
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import Calendar from '@lucide/svelte/icons/calendar';

// Composition Examples
import BoardExample from './pages/BoardExample.svelte'
Expand Down Expand Up @@ -102,6 +103,7 @@
import CategoryMultiSelectPage from './pages/CategoryMultiSelectPage.svelte'
import FieldSelectorPage from './pages/FieldSelectorPage.svelte'
import MentionPickerPage from './pages/MentionPickerPage.svelte'
import DatePickerPage from './pages/DatePickerPage.svelte'

let currentPage = $state(window.location.hash.slice(1) || 'board-example')
let colorMode = $state('light')
Expand Down Expand Up @@ -212,6 +214,7 @@
{ id: 'categorymultiselect', label: 'CategoryMultiSelect', icon: Tag },
{ id: 'fieldselector', label: 'FieldSelector', icon: List },
{ id: 'mentionpicker', label: 'MentionPicker', icon: AtSign },
{ id: 'datepicker', label: 'DatePicker', icon: Calendar },
]
},
]
Expand Down Expand Up @@ -363,6 +366,8 @@
<FieldSelectorPage />
{:else if currentPage === 'mentionpicker'}
<MentionPickerPage />
{:else if currentPage === 'datepicker'}
<DatePickerPage />
{/if}
</main>
</div>
71 changes: 71 additions & 0 deletions frontend/src/design-system/viewer/pages/DatePickerPage.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script>
import DatePicker from '../../../lib/pickers/DatePicker.svelte'

let basic = $state('')
let prefilled = $state('2026-11-07')
let persian = $state('2026-11-07')
let gregorian = $state('2026-11-07')
let bounded = $state('')
</script>

<div class="p-8 max-w-6xl">
<h1 class="text-2xl font-bold mb-2" style="color: var(--ds-text);">DatePicker</h1>
<p class="mb-8" style="color: var(--ds-text-subtle);">
A calendar-aware date picker built on Melt UI's headless calendar and
<code>@internationalized/date</code>. The bound value stays a Gregorian
<code>YYYY-MM-DD</code> string; the calendar follows the UI locale
(Persian for <code>fa</code>) unless overridden.
</p>

<section class="mb-10">
<h2 class="text-lg font-semibold mb-4" style="color: var(--ds-text);">Basic</h2>
<div class="max-w-xs">
<DatePicker bind:value={basic} label="Due date" dataTestid="ds-datepicker-basic" />
</div>
<p class="mt-4 text-xs" style="color: var(--ds-text-subtle);">Value: {basic || '—'}</p>
</section>

<section class="mb-10">
<h2 class="text-lg font-semibold mb-4" style="color: var(--ds-text);">Prefilled and clearable</h2>
<div class="max-w-xs">
<DatePicker bind:value={prefilled} label="Start date" dataTestid="ds-datepicker-prefilled" />
</div>
<p class="mt-4 text-xs" style="color: var(--ds-text-subtle);">Value: {prefilled || '—'}</p>
</section>

<section class="mb-10">
<h2 class="text-lg font-semibold mb-4" style="color: var(--ds-text);">Calendar override</h2>
<p class="mb-4 text-sm" style="color: var(--ds-text-subtle);">
Force the Persian or Gregorian calendar regardless of the UI locale.
</p>
<div class="grid grid-cols-2 gap-8 max-w-xl">
<DatePicker
bind:value={persian}
calendar="persian"
locale="en"
label="Persian (en)"
dataTestid="ds-datepicker-persian"
/>
<DatePicker
bind:value={gregorian}
calendar="gregory"
locale="en"
label="Gregorian (en)"
dataTestid="ds-datepicker-gregorian"
/>
</div>
</section>

<section class="mb-10">
<h2 class="text-lg font-semibold mb-4" style="color: var(--ds-text);">Bounded</h2>
<div class="max-w-xs">
<DatePicker
bind:value={bounded}
min="2026-11-01"
max="2026-11-30"
label="November only"
dataTestid="ds-datepicker-bounded"
/>
</div>
</section>
</div>
28 changes: 28 additions & 0 deletions frontend/src/lib/components/Input.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script>
import { cn } from '../utils/cn.js';
import { controlSizeClasses } from '../utils/controlSizes.js';
import DatePicker from '../pickers/DatePicker.svelte';

/**
* @type {{
Expand Down Expand Up @@ -104,6 +105,32 @@
</script>

<!-- svelte-ignore a11y_autofocus -->
{#if type === 'date'}
<DatePicker
bind:value
bind:inputRef
{id}
{name}
{min}
{max}
{disabled}
{required}
readonly={readonly}
{placeholder}
{size}
class={className}
{dataTestid}
{ariaLabel}
{onkeydown}
{onblur}
{onfocus}
onchange={(next) => {
const synthetic = { target: { value: next } };
oninput?.(synthetic);
onchange?.(synthetic);
}}
/>
{:else}
<input
{type}
{id}
Expand Down Expand Up @@ -139,3 +166,4 @@
{onkeydown}
{onkeyup}
/>
{/if}
24 changes: 6 additions & 18 deletions frontend/src/lib/features/items/SidebarDateField.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script>
import Calendar from '@lucide/svelte/icons/calendar';
import Text from '../../components/Text.svelte';
import DatePicker from '../../pickers/DatePicker.svelte';
import { clickOutside } from '../../actions/clickOutside.js';
import { t } from '../../stores/i18n.svelte.js';
import { formatDateOnly } from '../../utils/dateFormatter.js';
Expand All @@ -16,31 +17,18 @@
onCancel = () => {},
} = $props();

function focusAndShowPicker(node) {
node.focus();
setTimeout(() => {
try {
node.showPicker();
} catch {
// The browser may not support programmatic picker opening.
}
}, 0);
}

let testId = $derived(`sidebar-date-${fieldKey.replaceAll('_', '-')}`);
</script>

<div class="mb-1">
{#if editing}
<div class="w-full py-1.5" use:clickOutside onclickOutside={onCancel}>
<input
data-testid={`${testId}-input`}
type="date"
<DatePicker
inline
autoOpen
value={value ? value.split('T')[0] : ''}
onchange={(event) => onSave(event.currentTarget.value || null)}
class="w-full px-2 py-1 rounded-md text-sm focus:outline-none focus:ring-0"
style="background-color: transparent; color: var(--ds-text);"
use:focusAndShowPicker
onchange={(next) => onSave(next || null)}
dataTestid={`${testId}-input`}
/>
</div>
{:else}
Expand Down
52 changes: 19 additions & 33 deletions frontend/src/lib/forms/WorkItemForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { formatDateOnly, formatDueDate } from '../utils/dateFormatter.js';
import MilkdownEditor from '../editors/LazyMilkdownEditor.svelte';
import ChipPicker from '../pickers/ChipPicker.svelte';
import DatePicker from '../pickers/DatePicker.svelte';
import CustomFieldRenderer from '../features/items/CustomFieldRenderer.svelte';
import PriorityPicker from '../pickers/PriorityPicker.svelte';
import MilestoneCombobox from '../pickers/MilestoneCombobox.svelte';
Expand Down Expand Up @@ -83,18 +84,9 @@
forceVisible: true
});

// Optional dates are presented as chips. Open the browser's native calendar
// as soon as the chip mounts its input so setting a date remains a one-click
// interaction. The focused, visible input is the fallback when showPicker is
// unavailable or the browser declines the programmatic picker request.
function focusAndShowDatePicker(node) {
node.focus();
try {
node.showPicker?.();
} catch {
// Keep the focused input available for manual entry.
}
}
// Optional dates are presented as chips that open a popover holding the
// calendar-aware date field. Native locales open the OS picker on mount so
// setting a date stays a one-click interaction.

// Reactive effects for data loading based on form state

Expand Down Expand Up @@ -390,14 +382,12 @@
class="z-[70] rounded-lg shadow-lg p-3"
style="background-color: var(--ds-surface-raised); border: 1px solid var(--ds-border);"
>
<input
type="date"
data-testid="create-due-date-input"
<DatePicker
inline
autoOpen
bind:value={store.formData.due_date}
use:focusAndShowDatePicker
aria-label={t('createModal.dueDate')}
class="w-full px-3 py-2 rounded border text-sm"
style="background-color: var(--ds-background-input); border-color: var(--ds-border); color: var(--ds-text);"
dataTestid="create-due-date-input"
ariaLabel={t('createModal.dueDate')}
/>
</div>
{/if}
Expand All @@ -422,14 +412,12 @@
class="z-[70] rounded-lg shadow-lg p-3"
style="background-color: var(--ds-surface-raised); border: 1px solid var(--ds-border);"
>
<input
type="date"
data-testid="create-start-date-input"
<DatePicker
inline
autoOpen
bind:value={store.formData.start_date}
use:focusAndShowDatePicker
aria-label={t('common.startDate')}
class="w-full px-3 py-2 rounded border text-sm"
style="background-color: var(--ds-background-input); border-color: var(--ds-border); color: var(--ds-text);"
dataTestid="create-start-date-input"
ariaLabel={t('common.startDate')}
/>
</div>
{/if}
Expand All @@ -454,14 +442,12 @@
class="z-[70] rounded-lg shadow-lg p-3"
style="background-color: var(--ds-surface-raised); border: 1px solid var(--ds-border);"
>
<input
type="date"
data-testid="create-end-date-input"
<DatePicker
inline
autoOpen
bind:value={store.formData.end_date}
use:focusAndShowDatePicker
aria-label={t('common.endDate')}
class="w-full px-3 py-2 rounded border text-sm"
style="background-color: var(--ds-background-input); border-color: var(--ds-border); color: var(--ds-text);"
dataTestid="create-end-date-input"
ariaLabel={t('common.endDate')}
/>
</div>
{/if}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/locales/dateFormats.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const DATE_FORMATS = {
fr: { date: '02/01/2006', dateTime: '02/01/2006 15:04', time: '15:04' },
ko: { date: '2006. 1. 2.', dateTime: '2006. 1. 2. 15:04', time: '15:04' },
it: { date: '02/01/2006', dateTime: '02/01/2006, 15:04', time: '15:04' },
fa: { date: '2006/1/2', dateTime: '2006/1/2، 15:04', time: '15:04' },
};

/** Keys every locale entry must define. */
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/lib/locales/de/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,14 @@ export default {
showingOfTotal: 'Zeige {shown} von {total} — tippen zum Suchen',
selectIteration: 'Iteration auswählen',
noIteration: 'Keine Iteration',

// Date Picker
datePicker: {
previousMonth: 'Vorheriger Monat',
nextMonth: 'Nächster Monat',
openCalendar: 'Kalender öffnen',
selectDate: 'Datum auswählen...',
},
},
editors: {
enterText: 'Text eingeben...',
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/lib/locales/en/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ export default {

// Approval Set Picker
selectApprovalSet: 'Select approval set',

// Date Picker
datePicker: {
previousMonth: 'Previous month',
nextMonth: 'Next month',
openCalendar: 'Open calendar',
selectDate: 'Select date...',
},
},

editors: {
Expand Down
Loading
Loading