From f1d7f848cf7757b14851c29bc9231df0d868cc1c Mon Sep 17 00:00:00 2001 From: Grzegorz Kucmierz Date: Mon, 9 Feb 2026 20:33:36 +0100 Subject: [PATCH] Mobile dropdown: visibility and readability improvements; i18n locales filtered to fully translated; dynamic dropdown locales --- src/App.vue | 81 +++++++++++--------------------------- src/composables/useI18n.js | 38 +++++++++++------- 2 files changed, 46 insertions(+), 73 deletions(-) diff --git a/src/App.vue b/src/App.vue index 588bf3c..1dafba3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -13,7 +13,7 @@ import FixedBar from './components/FixedBar.vue'; // Main App Entry const store = usePuzzleStore(); -const { t, locale, setLocale } = useI18n(); +const { t, locale, setLocale, locales } = useI18n(); const showCustomModal = ref(false); const showGuide = ref(false); const deferredPrompt = ref(null); @@ -74,53 +74,9 @@ const languageFlags = { }; const languages = computed(() => { - const items = [ - { code: 'en', label: t('language.en') }, - { code: 'zh', label: t('language.zh') }, - { code: 'hi', label: t('language.hi') }, - { code: 'es', label: t('language.es') }, - { code: 'fr', label: t('language.fr') }, - { code: 'ar', label: t('language.ar') }, - { code: 'bn', label: t('language.bn') }, - { code: 'ru', label: t('language.ru') }, - { code: 'pt', label: t('language.pt') }, - { code: 'ur', label: t('language.ur') }, - { code: 'pl', label: t('language.pl') }, - { code: 'de', label: t('language.de') }, - { code: 'it', label: t('language.it') }, - { code: 'nl', label: t('language.nl') }, - { code: 'sv', label: t('language.sv') }, - { code: 'da', label: t('language.da') }, - { code: 'fi', label: t('language.fi') }, - { code: 'no', label: t('language.no') }, - { code: 'cs', label: t('language.cs') }, - { code: 'sk', label: t('language.sk') }, - { code: 'hu', label: t('language.hu') }, - { code: 'ro', label: t('language.ro') }, - { code: 'bg', label: t('language.bg') }, - { code: 'el', label: t('language.el') }, - { code: 'uk', label: t('language.uk') }, - { code: 'be', label: t('language.be') }, - { code: 'sr', label: t('language.sr') }, - { code: 'hr', label: t('language.hr') }, - { code: 'sl', label: t('language.sl') }, - { code: 'lt', label: t('language.lt') }, - { code: 'lv', label: t('language.lv') }, - { code: 'et', label: t('language.et') }, - { code: 'ga', label: t('language.ga') }, - { code: 'is', label: t('language.is') }, - { code: 'mt', label: t('language.mt') }, - { code: 'sq', label: t('language.sq') }, - { code: 'mk', label: t('language.mk') }, - { code: 'bs', label: t('language.bs') }, - { code: 'tr', label: t('language.tr') }, - { code: 'ca', label: t('language.ca') }, - { code: 'gl', label: t('language.gl') }, - { code: 'cy', label: t('language.cy') }, - { code: 'gd', label: t('language.gd') }, - { code: 'eu', label: t('language.eu') } - ]; - return items.sort((a, b) => a.label.localeCompare(b.label, locale.value)); + return locales.value + .map((code) => ({ code, label: t(`language.${code}`) })) + .sort((a, b) => a.label.localeCompare(b.label, locale.value)); }); const installLabel = computed(() => { @@ -574,6 +530,23 @@ h1 { font-size: 2.4rem; letter-spacing: 3px; } + .lang-menu { + position: fixed; + top: 64px; + left: 50%; + right: auto; + transform: translateX(-50%); + width: min(92vw, 340px); + min-width: 240px; + z-index: 1005; + max-height: calc(100vh - 96px); + padding: 12px; + border-radius: 18px; + } + .lang-option { + font-size: 1rem; + padding: 10px 12px; + } } @media (max-width: 420px) { @@ -582,17 +555,7 @@ h1 { letter-spacing: 2px; } .lang-menu { - position: fixed; - top: 64px; - left: 50%; - right: auto; - transform: translateX(-50%); - width: min(90vw, 320px); - min-width: 240px; - z-index: 1005; - max-height: calc(100vh - 96px); - padding: 12px; - border-radius: 18px; + width: min(94vw, 360px); } } diff --git a/src/composables/useI18n.js b/src/composables/useI18n.js index acb9f49..bee782f 100644 --- a/src/composables/useI18n.js +++ b/src/composables/useI18n.js @@ -1,18 +1,5 @@ import { ref, computed } from 'vue'; -const supportedLocales = [ - 'pl','en','zh','hi','es','fr','ar','bn','ru','pt','ur', - 'de','it','nl','sv','da','fi','no','cs','sk','hu','ro','bg','el','uk','be', - 'sr','hr','sl','lt','lv','et','ga','is','mt','sq','mk','bs','tr','ca','gl','cy','gd','eu' -]; - -const detectLocale = () => { - if (typeof navigator === 'undefined') return 'en'; - const browserLocale = (navigator.languages && navigator.languages[0]) || navigator.language || 'en'; - const short = browserLocale.toLowerCase().split('-')[0]; - return supportedLocales.includes(short) ? short : 'en'; -}; - const messages = { pl: { 'app.title': 'Nonograms', @@ -787,6 +774,28 @@ const messages = { } }; +const requiredKeys = [ + 'app.title','level.easy','level.medium','level.hard','level.custom','level.guide', + 'actions.reset','actions.random','actions.undo','status.time','status.moves','status.progress', + 'fixed.time','fixed.progress','fixed.hide','fixed.show','guide.play','guide.pause','guide.step', + 'guide.speed','guide.waiting','guide.solved','custom.title','custom.prompt','custom.cancel', + 'custom.start','custom.sizeError','win.title','win.message','win.time','win.playAgain', + 'win.shareTitle','win.shareText','win.shareX','win.shareFacebook','win.shareWhatsapp', + 'win.shareDownload','pwa.installTitle','pwa.installMobile','pwa.installDesktop', + 'language.label','theme.label','theme.system','theme.light','theme.dark' +]; + +const supportedLocales = Object.keys(messages).filter( + (code) => requiredKeys.every((k) => messages[code] && messages[code][k]) +); + +const detectLocale = () => { + if (typeof navigator === 'undefined') return 'en'; + const browserLocale = (navigator.languages && navigator.languages[0]) || navigator.language || 'en'; + const short = browserLocale.toLowerCase().split('-')[0]; + return supportedLocales.includes(short) ? short : 'en'; +}; + const locale = ref(detectLocale()); const format = (text, params = {}) => { @@ -819,6 +828,7 @@ export function useI18n() { return { locale: computed(() => locale.value), t, - setLocale + setLocale, + locales: computed(() => supportedLocales) }; }