diff --git a/.env.example b/.env.example index ece51f9..2c30e82 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,8 @@ VUE_APP_TITLE=irandom VUE_APP_BRAND_URL=https://git.imiheev.online/admin/irandom # Logo path or absolute URL (file should be in public/ if relative) VUE_APP_LOGO_URL=/logo.png +# Default UI language: ru | en (overridden by localStorage after user chooses) +VUE_APP_DEFAULT_LOCALE=ru # Theme colors (CSS hex) VUE_APP_COLOR_PRIMARY=#c9a66b diff --git a/package-lock.json b/package-lock.json index b2773fa..a362b0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@element-plus/icons-vue": "^2.3.2", "element-plus": "^2.14.3", "vue": "^3.5.13", + "vue-i18n": "^11.4.8", "vue-router": "^4.6.4" }, "devDependencies": { @@ -653,6 +654,67 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@intlify/core-base": { + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.8.tgz", + "integrity": "sha512-A+Q7SKm5oEcy1E/cghqd7n/St4XjTqLhiiyDuieNcMrJcrHlkY5n0jp7Q9dD3txvVHzvsmBVV5M9wD5/s1zfzw==", + "license": "MIT", + "dependencies": { + "@intlify/devtools-types": "11.4.8", + "@intlify/message-compiler": "11.4.8", + "@intlify/shared": "11.4.8" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/devtools-types": { + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.8.tgz", + "integrity": "sha512-MGpID+rlfzGUbNcnC20bm5NMSBHPrvx0atLTfv9dftn3kjXw1hGKDcIcwrO99tSrZEc2i+hczRL7ks8qXsHPkQ==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.4.8", + "@intlify/shared": "11.4.8" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.8.tgz", + "integrity": "sha512-vbzk17dYwduYiv52EK61+FDCyhfVg1uPUtPmiD/d45W99uJIcXywrweOBcHv7n9/iEqmXiMGT52bgJbZDQqK3w==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "11.4.8", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.8.tgz", + "integrity": "sha512-XbRgrv+XEuvDr7UCY55oibVrh+o4u+A0VB6nSL0F5Z8LcZxE/8j573LYG6bCrOigIcHdGpSNI7Rh5UpC5/B/eg==", + "license": "MIT", + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -3201,6 +3263,27 @@ "eslint": ">=6.0.0" } }, + "node_modules/vue-i18n": { + "version": "11.4.8", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.8.tgz", + "integrity": "sha512-0ULeHP6Z9CGvAm67S77ZEp41cfGXIREGL8qfhos2BMgcQQewtQcDKuojt6jjasAD/S8GwfTp2ySPmDSpwvrCMQ==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.4.8", + "@intlify/devtools-types": "11.4.8", + "@intlify/shared": "11.4.8", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/vue-router": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", diff --git a/package.json b/package.json index ed867c9..e5d9f5d 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@element-plus/icons-vue": "^2.3.2", "element-plus": "^2.14.3", "vue": "^3.5.13", + "vue-i18n": "^11.4.8", "vue-router": "^4.6.4" }, "devDependencies": { diff --git a/public/config.js b/public/config.js index 87054a4..d1637fe 100644 --- a/public/config.js +++ b/public/config.js @@ -4,6 +4,7 @@ window.__APP_CONFIG__ = { "title": "irandom", "brandUrl": "https://git.imiheev.online/admin/irandom", "logoUrl": "/logo.png", + "defaultLocale": "ru", "colors": { "primary": "#c9a66b", "accent": "#e6c566", diff --git a/scripts/generate-config.js b/scripts/generate-config.js index 1ac1e8d..4512e35 100644 --- a/scripts/generate-config.js +++ b/scripts/generate-config.js @@ -10,6 +10,7 @@ const DEFAULTS = { VUE_APP_TITLE: 'irandom', VUE_APP_BRAND_URL: 'https://git.imiheev.online/admin/irandom', VUE_APP_LOGO_URL: '/logo.png', + VUE_APP_DEFAULT_LOCALE: 'ru', VUE_APP_COLOR_PRIMARY: '#c9a66b', VUE_APP_COLOR_ACCENT: '#e6c566', VUE_APP_COLOR_BG: '#1a1815', @@ -63,6 +64,7 @@ function buildConfig(env) { title: pick('VUE_APP_TITLE'), brandUrl: pick('VUE_APP_BRAND_URL'), logoUrl: pick('VUE_APP_LOGO_URL'), + defaultLocale: pick('VUE_APP_DEFAULT_LOCALE'), colors: { primary: pick('VUE_APP_COLOR_PRIMARY'), accent: pick('VUE_APP_COLOR_ACCENT'), diff --git a/src/App.vue b/src/App.vue index 859abe6..b562e64 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,23 +1,28 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - + @@ -40,6 +67,7 @@ export default { background: var(--ir-surface); overflow: hidden; padding: 0; + transition: width 0.2s ease; .mode-sidebar { height: 100%; @@ -68,6 +96,11 @@ export default { display: none; } +.ir-lang-popper { + background: var(--ir-surface) !important; + border: 1px solid var(--ir-border) !important; +} + @media (max-width: 424px) { .app-shell { flex-direction: column !important; diff --git a/src/components/ModeSidebar.vue b/src/components/ModeSidebar.vue index 8a17534..df576e4 100644 --- a/src/components/ModeSidebar.vue +++ b/src/components/ModeSidebar.vue @@ -1,5 +1,5 @@ - + - {{ config.name }} + {{ config.name }} - - - + + + - {{ mode.label }} - + + + + + + + + {{ t(mode.labelKey) }} + + + + + + {{ t('nav.language') }} + + + {{ t('lang.ru') }} + + + {{ t('lang.en') }} + + + @@ -59,9 +171,9 @@ export default { .mode-sidebar { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.35rem; height: 100%; - padding: 1rem 0.75rem; + padding: 1rem 0.5rem; background: var(--ir-surface); border-right: 1px solid var(--ir-border); } @@ -70,10 +182,11 @@ export default { display: flex; align-items: center; gap: 0.65rem; - padding: 0.35rem 0.5rem 1rem; - margin-bottom: 0.25rem; + padding: 0.35rem 0.5rem 0.85rem; + margin-bottom: 0.15rem; border-bottom: 1px solid var(--ir-border); text-decoration: none; + overflow: hidden; } .logo { @@ -90,93 +203,120 @@ export default { letter-spacing: 0.02em; color: var(--ir-primary); transition: color 0.15s ease; + white-space: nowrap; } .brand:hover .brand-name { color: color-mix(in srgb, var(--ir-primary) 70%, white); } -.mode-item { - display: flex; - align-items: center; - gap: 0.65rem; +.collapse-btn { + align-self: flex-end; appearance: none; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--ir-text-muted); - padding: 0.85rem 0.75rem; - text-align: left; - text-decoration: none; - font: inherit; - font-size: 0.95rem; + padding: 0.45rem; cursor: pointer; - transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; + transition: background 0.15s ease, color 0.15s ease; &:hover { background: var(--ir-surface-raised); color: var(--ir-text); } - - &.active { - background: rgba(var(--ir-primary-rgb), 0.18); - border-color: var(--ir-primary); - color: var(--ir-primary); - font-weight: 600; - } } -.mode-icon { - flex-shrink: 0; +.side-menu { + border-right: none !important; + width: 100%; + background: transparent !important; + flex: 1; +} + +.side-menu:not(.el-menu--collapse) { + width: 100%; +} + +:deep(.el-menu-item), +:deep(.el-sub-menu__title) { + border-radius: 8px; + margin: 0.15rem 0; + height: 44px; + line-height: 44px; +} + +:deep(.el-menu-item.is-active) { + background: rgba(var(--ir-primary-rgb), 0.18) !important; + border: 1px solid var(--ir-primary); + font-weight: 600; +} + +:deep(.el-menu-item:hover), +:deep(.el-sub-menu__title:hover) { + background: var(--ir-surface-raised) !important; +} + +:deep(.el-menu-item.is-locale-active) { + color: var(--ir-primary) !important; + font-weight: 600; +} + +.collapsed { + .brand { + justify-content: center; + padding-left: 0.25rem; + padding-right: 0.25rem; + } + + .collapse-btn { + align-self: center; + } } @media (max-width: 424px) { .mode-sidebar { flex-direction: row; align-items: stretch; - justify-content: space-around; - gap: 0; height: auto; - padding: 0.35rem 0.25rem calc(0.35rem + env(safe-area-inset-bottom, 0px)); + padding: 0.2rem 0.15rem calc(0.2rem + env(safe-area-inset-bottom, 0px)); border-right: none; border-top: 1px solid var(--ir-border); box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.35); + gap: 0; } - .mode-item { + .side-menu { + display: flex; + flex-direction: row; + width: 100%; + justify-content: space-around; + } + + :deep(.el-menu-item), + :deep(.el-sub-menu) { flex: 1; + } + + :deep(.el-menu-item), + :deep(.el-sub-menu__title) { + display: flex; flex-direction: column; + align-items: center; justify-content: center; - gap: 0.2rem; - padding: 0.55rem 0.25rem; - border-radius: 10px; - text-align: center; - font-size: 0.7rem; - border-color: transparent; - background: transparent; - - &:hover { - background: transparent; - color: var(--ir-text); - } - - &.active { - background: transparent; - border-color: transparent; - color: var(--ir-primary); - } - } - - .mode-icon { - font-size: 1.35rem; - } - - .mode-label { + height: auto; line-height: 1.2; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + padding: 0.45rem 0.2rem !important; + border: none !important; + font-size: 0.65rem; + } + + :deep(.el-menu-item.is-active) { + background: transparent !important; + } + + :deep(.el-sub-menu .el-sub-menu__icon-arrow) { + display: none; } } diff --git a/src/components/ResultDisplay.vue b/src/components/ResultDisplay.vue index f7a2e26..2d9b035 100644 --- a/src/components/ResultDisplay.vue +++ b/src/components/ResultDisplay.vue @@ -1,6 +1,6 @@ - Результат + {{ t('result.label') }} @@ -11,11 +11,13 @@ {{ result }} - Нажмите «Сгенерировать» + {{ t('result.empty') }}