diff --git a/status/js/language-selector.js b/status/js/language-selector.js
index 9cb91c6..4e73852 100644
--- a/status/js/language-selector.js
+++ b/status/js/language-selector.js
@@ -2,12 +2,13 @@
* DC-077: i18n Language Selector
*
* Compact dropdown in the navbar (next to the theme toggle) that lets users switch
- * the dashboard language between en / es / zh / ar / de.
+ * the dashboard language. Supports all 31 backend languages with a searchable list.
*
* - Shows current language with flag emoji
* - Persists selection to localStorage('dashcaddy-language')
* - Sends selection to backend via POST /api/v1/config with { language: 'xx' }
* - Reloads the page on change so the new language takes effect
+ * - Search filter for quickly finding a language in the 31-option list
*
* Depends on: secureFetch() / postJSON() from globals.js (bundled in /dist/core.js).
*/
@@ -18,11 +19,37 @@
const CONFIG_ENDPOINT = '/api/v1/config';
const LANGUAGES = [
- { code: 'en', flag: '🇺🇸', label: 'English' },
- { code: 'es', flag: '🇪🇸', label: 'Español' },
- { code: 'zh', flag: '🇨🇳', label: '中文' },
- { code: 'ar', flag: '🇸🇦', label: 'العربية' },
- { code: 'de', flag: '🇩🇪', label: 'Deutsch' },
+ { code: 'en', flag: '🇬🇧', label: 'English', nativeLabel: 'English' },
+ { code: 'ar', flag: '🇸🇦', label: 'Arabic', nativeLabel: 'العربية' },
+ { code: 'bn', flag: '🇧🇩', label: 'Bengali', nativeLabel: 'বাংলা' },
+ { code: 'cs', flag: '🇨🇿', label: 'Czech', nativeLabel: 'Čeština' },
+ { code: 'da', flag: '🇩🇰', label: 'Danish', nativeLabel: 'Dansk' },
+ { code: 'de', flag: '🇩🇪', label: 'German', nativeLabel: 'Deutsch' },
+ { code: 'el', flag: '🇬🇷', label: 'Greek', nativeLabel: 'Ελληνικά' },
+ { code: 'es', flag: '🇪🇸', label: 'Spanish', nativeLabel: 'Español' },
+ { code: 'fa', flag: '🇮🇷', label: 'Persian', nativeLabel: 'فارسی' },
+ { code: 'fi', flag: '🇫🇮', label: 'Finnish', nativeLabel: 'Suomi' },
+ { code: 'fr', flag: '🇫🇷', label: 'French', nativeLabel: 'Français' },
+ { code: 'hi', flag: '🇮🇳', label: 'Hindi', nativeLabel: 'हिन्दी' },
+ { code: 'hu', flag: '🇭🇺', label: 'Hungarian', nativeLabel: 'Magyar' },
+ { code: 'id', flag: '🇮🇩', label: 'Indonesian', nativeLabel: 'Bahasa Indonesia' },
+ { code: 'it', flag: '🇮🇹', label: 'Italian', nativeLabel: 'Italiano' },
+ { code: 'ja', flag: '🇯🇵', label: 'Japanese', nativeLabel: '日本語' },
+ { code: 'ko', flag: '🇰🇷', label: 'Korean', nativeLabel: '한국어' },
+ { code: 'ms', flag: '🇲🇾', label: 'Malay', nativeLabel: 'Bahasa Melayu' },
+ { code: 'nl', flag: '🇳🇱', label: 'Dutch', nativeLabel: 'Nederlands' },
+ { code: 'no', flag: '🇳🇴', label: 'Norwegian', nativeLabel: 'Norsk' },
+ { code: 'pl', flag: '🇵🇱', label: 'Polish', nativeLabel: 'Polski' },
+ { code: 'pt', flag: '🇵🇹', label: 'Portuguese', nativeLabel: 'Português' },
+ { code: 'ro', flag: '🇷🇴', label: 'Romanian', nativeLabel: 'Română' },
+ { code: 'ru', flag: '🇷🇺', label: 'Russian', nativeLabel: 'Русский' },
+ { code: 'sv', flag: '🇸🇪', label: 'Swedish', nativeLabel: 'Svenska' },
+ { code: 'th', flag: '🇹🇭', label: 'Thai', nativeLabel: 'ไทย' },
+ { code: 'tr', flag: '🇹🇷', label: 'Turkish', nativeLabel: 'Türkçe' },
+ { code: 'uk', flag: '🇺🇦', label: 'Ukrainian', nativeLabel: 'Українська' },
+ { code: 'ur', flag: '🇵🇰', label: 'Urdu', nativeLabel: 'اردو' },
+ { code: 'vi', flag: '🇻🇳', label: 'Vietnamese', nativeLabel: 'Tiếng Việt' },
+ { code: 'zh', flag: '🇨🇳', label: 'Chinese', nativeLabel: '中文' },
];
const SUPPORTED = LANGUAGES.map(l => l.code);
@@ -82,7 +109,10 @@
position: absolute;
top: calc(100% + 6px);
right: 0;
- min-width: 160px;
+ min-width: 200px;
+ max-height: 360px;
+ display: flex;
+ flex-direction: column;
background: var(--card-base, #1e1e2e);
border: 1px solid var(--border);
border-radius: 8px;
@@ -92,7 +122,35 @@
display: none;
}
.dc-lang-menu.open {
- display: block;
+ display: flex;
+ }
+ .dc-lang-search {
+ margin: 2px 0 6px;
+ padding: 6px 10px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--bg-base, #111);
+ color: var(--fg);
+ font-size: 0.82rem;
+ font-family: inherit;
+ outline: none;
+ width: 100%;
+ box-sizing: border-box;
+ }
+ .dc-lang-search:focus {
+ border-color: var(--accent);
+ }
+ .dc-lang-list {
+ overflow-y: auto;
+ max-height: 280px;
+ scrollbar-width: thin;
+ }
+ .dc-lang-list::-webkit-scrollbar {
+ width: 5px;
+ }
+ .dc-lang-list::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 3px;
}
.dc-lang-option {
display: flex;
@@ -126,6 +184,11 @@
.dc-lang-option.active .dc-lang-check {
opacity: 1;
}
+ .dc-lang-option.dc-lang-focus,
+ .dc-lang-option:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: -2px;
+ }
.dc-lang-label-sm {
background: none !important;
border: none !important;
@@ -146,19 +209,111 @@
menu.className = 'dc-lang-menu';
menu.setAttribute('role', 'menu');
+ // Search input
+ const search = document.createElement('input');
+ search.type = 'text';
+ search.className = 'dc-lang-search';
+ search.placeholder = 'Search language…';
+ search.setAttribute('aria-label', 'Search languages');
+ search.autocomplete = 'off';
+
+ // Scrollable option list
+ const list = document.createElement('div');
+ list.className = 'dc-lang-list';
+
for (const lang of LANGUAGES) {
const opt = document.createElement('div');
opt.className = 'dc-lang-option' + (lang.code === current ? ' active' : '');
opt.setAttribute('role', 'menuitemradio');
opt.setAttribute('aria-checked', lang.code === current ? 'true' : 'false');
+ opt.setAttribute('tabindex', '-1');
opt.dataset.lang = lang.code;
+ opt.dataset.search = (lang.label + ' ' + lang.nativeLabel + ' ' + lang.code).toLowerCase();
opt.innerHTML =
'' + lang.flag + '' +
- '' + lang.label + '' +
+ '' + lang.nativeLabel +
+ '' + lang.label + '' +
+ '' +
'✓';
- menu.appendChild(opt);
+ list.appendChild(opt);
}
- return menu;
+
+ // Filter logic — extracted so we can reset from the open handler
+ function applyFilter(query) {
+ var q = (query || '').toLowerCase().trim();
+ list.querySelectorAll('.dc-lang-option').forEach(function (opt) {
+ var match = !q || opt.dataset.search.indexOf(q) !== -1;
+ opt.style.display = match ? '' : 'none';
+ });
+ }
+
+ search.addEventListener('input', function () {
+ applyFilter(this.value);
+ });
+
+ // Prevent clicks on search from closing the menu
+ search.addEventListener('click', function (e) { e.stopPropagation(); });
+
+ // Expose reset so init() can restore visibility when reopening
+ menu._resetFilter = function () {
+ search.value = '';
+ applyFilter('');
+ };
+
+ // ===== Keyboard navigation (Arrow Up/Down, Enter, Space) =====
+ function getVisibleOptions() {
+ return Array.from(list.querySelectorAll('.dc-lang-option')).filter(
+ function (o) { return o.style.display !== 'none'; }
+ );
+ }
+
+ function focusOption(opt) {
+ if (!opt) return;
+ var visible = getVisibleOptions();
+ visible.forEach(function (o) { o.classList.remove('dc-lang-focus'); });
+ opt.classList.add('dc-lang-focus');
+ opt.focus();
+ }
+
+ search.addEventListener('keydown', function (e) {
+ var visible = getVisibleOptions();
+ if (visible.length === 0) return;
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ focusOption(visible[0]);
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ var active = list.querySelector('.dc-lang-option.active');
+ if (active && active.style.display !== 'none') selectLanguage(active.dataset.lang);
+ }
+ });
+
+ list.addEventListener('keydown', function (e) {
+ var visible = getVisibleOptions();
+ var currentIdx = visible.indexOf(document.activeElement);
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ var next = visible[Math.min(currentIdx + 1, visible.length - 1)];
+ focusOption(next);
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ if (currentIdx === 0) {
+ search.focus();
+ } else {
+ focusOption(visible[currentIdx - 1]);
+ }
+ } else if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ var opt = document.activeElement;
+ if (opt && opt.classList.contains('dc-lang-option')) {
+ selectLanguage(opt.dataset.lang);
+ }
+ }
+ });
+
+ menu.appendChild(search);
+ menu.appendChild(list);
+ return { menu: menu, search: search };
}
async function selectLanguage(code) {
@@ -207,7 +362,9 @@
'' + current.toUpperCase() + '' +
'▼';
- const menu = buildMenu(current);
+ const built = buildMenu(current);
+ const menu = built.menu;
+ const searchInput = built.search;
// Small label beneath, matching the "Customize Theme" link style
const label = document.createElement('span');
@@ -223,9 +380,16 @@
e.stopPropagation();
const isOpen = menu.classList.toggle('open');
btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
+ if (isOpen) {
+ // Reset filter: clear search text AND restore all hidden options
+ if (typeof menu._resetFilter === 'function') {
+ menu._resetFilter();
+ }
+ searchInput.focus();
+ }
});
- // Option clicks
+ // Option clicks (delegate to the list container)
menu.addEventListener('click', (e) => {
const opt = e.target.closest('.dc-lang-option');
if (!opt) return;
@@ -243,11 +407,12 @@
}
});
- // Close on Escape
+ // Close on Escape — return focus to the trigger button for accessibility
document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') {
+ if (e.key === 'Escape' && menu.classList.contains('open')) {
menu.classList.remove('open');
btn.setAttribute('aria-expanded', 'false');
+ btn.focus();
}
});