Features: 1) Add Pinia stores for language, company, and cart management; 2) Implement new i18n plugin with dynamic locale detection and improved setup; 3) Enhance routing logic with locale-aware redirects;

Fixes: 1) Replace usage of `vue-router` with `window.location.href` for redirects across multiple composables; 2) Ensure proper locale switching in authentication flows;

Extra: 1) Update `package-lock.json` with dependencies for Apollo, Vue I18n, and styling; 2) Remove unused `i18n.config.js` to streamline i18n setup; 3) General composables refactoring to improve code maintainability;
This commit is contained in:
Alexandr SaVBaD Waltz 2025-06-01 16:52:36 +03:00
parent 8a8a1605ea
commit fd8774b817
22 changed files with 1758 additions and 140 deletions

View file

@ -1,9 +1,42 @@
// @ts-check
import node from '@astrojs/node';
import { defineConfig } from 'astro/config';
import vue from '@astrojs/vue';
import { fileURLToPath, URL } from 'node:url'
// https://astro.build/config
export default defineConfig({
integrations: [vue()]
integrations: [
vue({
appEntrypoint: '/src/plugins/index.js',
devtools: { launchEditor: "webstorm" }
})
],
output: 'server',
adapter: node({
mode: 'standalone'
}),
vite: {
envDir: '../',
envPrefix: 'EVIBES_',
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@core': fileURLToPath(new URL('./src/core', import.meta.url)),
'@graphql': fileURLToPath(new URL('./src/graphql', import.meta.url)),
'@styles': fileURLToPath(new URL('./src/assets/styles', import.meta.url)),
'@icons': fileURLToPath(new URL('./src/assets/icons', import.meta.url)),
'@images': fileURLToPath(new URL('./src/assets/images', import.meta.url)),
}
},
css: {
preprocessorOptions: {
scss: {
additionalData: `
@use "@/assets/styles/global/variables.scss" as *;
@use "@/assets/styles/global/mixins.scss" as *;
`
}
}
},
}
});

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
{
"name": "evibes",
"name": "evibes-storefront",
"type": "module",
"version": "0.0.1",
"scripts": {
@ -9,8 +9,24 @@
"astro": "astro"
},
"dependencies": {
"@apollo/client": "^3.13.8",
"@astrojs/node": "^9.2.2",
"@astrojs/vue": "^5.1.0",
"@vue/apollo-composable": "^4.2.2",
"@vueuse/core": "^13.2.0",
"astro": "^5.8.1",
"vue": "^3.5.16"
"element-plus": "^2.9.11",
"graphql": "^16.11.0",
"graphql-tag": "^2.12.6",
"pinia": "^3.0.1",
"primeicons": "^7.0.0",
"swiper": "^11.2.8",
"vue": "^3.5.16",
"vue-i18n": "^11.1.4",
"vue3-marquee-slider": "^1.0.5"
},
"devDependencies": {
"sass": "^1.83.0",
"sass-loader": "^16.0.4"
}
}

View file

@ -64,10 +64,8 @@
import {useI18n} from "vue-i18n";
import HeaderSearchSkeleton from "@/components/skeletons/header/header-search-skeleton.vue";
import { useSearchUI } from "@/composables/search";
import {useRouter} from "vue-router";
const {t} = useI18n();
const router = useRouter();
const {
query,
@ -83,10 +81,7 @@ const {
function submitSearch() {
if (query.value) {
router.push({
name: 'search',
query: { q: query.value }
});
window.location.href = `/search?q=${encodeURIComponent(query.value)}`;
toggleSearch(false);
}

View file

@ -10,13 +10,10 @@ import {
LOCALE_STORAGE_LOCALE_KEY,
LOCALE_STORAGE_REFRESH_TOKEN_KEY,
} from "@/config/index.js";
import {useRoute, useRouter} from "vue-router";
import {usePendingOrder} from "@/composables/orders";
import {useWishlist} from "@/composables/wishlist";
export function useLogin() {
const router = useRouter();
const route = useRoute();
const userStore = useUserStore()
const {t} = useI18n();
@ -55,19 +52,17 @@ export function useLogin() {
type: 'success'
})
await router.push({
name: 'home',
params: {
locale: localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY) || DEFAULT_LOCALE
}
})
let locale = localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY) || DEFAULT_LOCALE;
if (response.data.obtainJwtToken.user.language !== translations.currentLocale) {
translations.switchLanguage(response.data.obtainJwtToken.user.language, router, route)
translations.switchLanguage(response.data.obtainJwtToken.user.language);
locale = response.data.obtainJwtToken.user.language;
}
await getPendingOrder(response.data.obtainJwtToken.user.email);
await getWishlist();
window.location.href = `/${locale}/`;
}
} catch (error) {
console.error("useLogin error:", error);

View file

@ -4,11 +4,9 @@ import {
LOCALE_STORAGE_LOCALE_KEY,
LOCALE_STORAGE_REFRESH_TOKEN_KEY
} from "@/config/index.js";
import {useRouter} from "vue-router";
export function useLogout() {
const userStore = useUserStore()
const router = useRouter()
async function logout() {
userStore.setUser({
@ -19,12 +17,9 @@ export function useLogout() {
localStorage.removeItem(LOCALE_STORAGE_REFRESH_TOKEN_KEY)
localStorage.removeItem(LOCALE_STORAGE_ACCESS_TOKEN_KEY)
await router.push({
name: 'home',
params: {
locale: localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY) || DEFAULT_LOCALE
}
})
const locale = localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY) || DEFAULT_LOCALE;
window.location.href = `/${locale}/`;
}
return {

View file

@ -3,13 +3,10 @@ import {NEW_PASSWORD} from "@/graphql/mutations/auth.js";
import {computed, ref} from "vue";
import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
import {useRoute, useRouter} from "vue-router";
import {DEFAULT_LOCALE, LOCALE_STORAGE_LOCALE_KEY} from "@/config/index.js";
export function useNewPassword() {
const {t} = useI18n();
const route = useRoute();
const router = useRouter();
const { mutate: newPasswordMutation } = useMutation(NEW_PASSWORD);

View file

@ -5,14 +5,11 @@ import {ElNotification} from "element-plus";
import {useI18n} from "vue-i18n";
import {useUserStore} from "@/stores/user.js";
import {LOCALE_STORAGE_REFRESH_TOKEN_KEY} from "@/config/index.js";
import {useRoute, useRouter} from "vue-router";
import translations from "@/core/helpers/translations.js";
import {usePendingOrder} from "@/composables/orders";
import {useWishlist} from "@/composables/wishlist";
export function useRefresh() {
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const {t} = useI18n();
@ -42,7 +39,7 @@ export function useRefresh() {
})
if (response.data.refreshJwtToken.user.language !== translations.currentLocale) {
translations.switchLanguage(response.data.refreshJwtToken.user.language, router, route)
translations.switchLanguage(response.data.refreshJwtToken.user.language)
}
localStorage.setItem(LOCALE_STORAGE_REFRESH_TOKEN_KEY, response.data.refreshJwtToken.refreshToken)

View file

@ -8,7 +8,7 @@ export const APP_NAME_KEY = APP_NAME.toLowerCase()
// LOCALES
export const SUPPORTED_LOCALES = [
export const SUPPORTED_LOCALES = [
{
code: 'en-gb',
default: true

View file

@ -3,7 +3,7 @@ export async function loadLocaleMessages(locale) {
const messages = await import(`../locales/${locale}.json`)
return messages.default || messages
} catch (error) {
console.error(`Не удалось загрузить локаль: ${locale}`, error)
console.error(`Failed to load locale: ${locale}`, error)
return {}
}
}
@ -21,7 +21,7 @@ export async function loadAllLocaleMessages(supportedLocales) {
const localeMessages = await import(`../../locales/${locale.code}.json`)
messages[locale.code] = localeMessages.default || localeMessages
} catch (error) {
console.error(`Не удалось загрузить локаль: ${locale.code}`, error)
console.error(`Failed to load locale: ${locale.code}`, error)
messages[locale.code] = {}
}
}

View file

@ -1,36 +1,26 @@
import i18n from '@/core/plugins/i18n.config';
import i18n from '@/plugins/i18n.js';
import {DEFAULT_LOCALE, LOCALE_STORAGE_LOCALE_KEY, SUPPORTED_LOCALES} from "@/config/index.js";
const translations = {
get currentLocale() {
return i18n.global.locale.value
return i18n.global?.locale?.value || DEFAULT_LOCALE.code;
},
set currentLocale(newLocale) {
i18n.global.locale.value = newLocale
if (i18n.global?.locale) {
i18n.global.locale.value = newLocale;
}
},
switchLanguage(newLocale, router = null, route = null) {
translations.currentLocale = newLocale
switchLanguage(newLocale) {
translations.currentLocale = newLocale;
document.querySelector('html').setAttribute('lang', newLocale)
if (typeof document !== 'undefined') {
document.querySelector('html')?.setAttribute('lang', newLocale);
}
localStorage.setItem(LOCALE_STORAGE_LOCALE_KEY, newLocale)
if (router && route) {
const newRoute = {
...route,
params: {
...route.params,
locale: newLocale
}
};
router.push(newRoute).catch(err => {
if (err.name !== 'NavigationDuplicated') {
console.error('Navigation error:', err);
}
});
if (typeof localStorage !== 'undefined') {
localStorage.setItem(LOCALE_STORAGE_LOCALE_KEY, newLocale);
}
},
@ -38,70 +28,95 @@ const translations = {
if (locale) {
return SUPPORTED_LOCALES.some(supportedLocale => supportedLocale.code === locale);
}
return false
return false;
},
getUserLocale() {
const locale =
window.navigator.language ||
DEFAULT_LOCALE.code
if (typeof window === 'undefined') {
return {
locale: DEFAULT_LOCALE.code,
localeNoRegion: DEFAULT_LOCALE.code.split('-')[0]
};
}
const locale = window.navigator.language || DEFAULT_LOCALE.code;
return {
locale: locale,
localeNoRegion: locale.split('-')[0]
}
};
},
getPersistedLocale() {
const persistedLocale = localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY)
if (typeof localStorage === 'undefined') {
return null;
}
const persistedLocale = localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY);
if (translations.isLocaleSupported(persistedLocale)) {
return persistedLocale
return persistedLocale;
} else {
return null
return null;
}
},
guessDefaultLocale() {
const userPersistedLocale = translations.getPersistedLocale()
const userPersistedLocale = translations.getPersistedLocale();
if (userPersistedLocale) {
return userPersistedLocale
return userPersistedLocale;
}
const userPreferredLocale = translations.getUserLocale()
if (typeof window !== 'undefined') {
const userPreferredLocale = translations.getUserLocale();
if (translations.isLocaleSupported(userPreferredLocale.locale)) {
return userPreferredLocale.locale
}
if (translations.isLocaleSupported(userPreferredLocale.locale)) {
return userPreferredLocale.locale;
}
if (translations.isLocaleSupported(userPreferredLocale.localeNoRegion)) {
return userPreferredLocale.localeNoRegion
}
return DEFAULT_LOCALE.code
},
async routeMiddleware(to, _from, next) {
const paramLocale = to.params.locale
if (!translations.isLocaleSupported(paramLocale)) {
return next(translations.guessDefaultLocale())
}
await translations.switchLanguage(paramLocale)
return next()
},
i18nRoute(to) {
return {
...to,
params: {
locale: translations.currentLocale,
...to.params
if (translations.isLocaleSupported(userPreferredLocale.localeNoRegion)) {
return userPreferredLocale.localeNoRegion;
}
}
}
}
export default translations
return DEFAULT_LOCALE.code;
},
getLocaleFromUrl(url) {
const pathParts = new URL(url).pathname.split('/').filter(Boolean);
if (pathParts.length > 0) {
const possibleLocale = pathParts[0];
if (translations.isLocaleSupported(possibleLocale)) {
return possibleLocale;
}
}
return DEFAULT_LOCALE.code;
},
getLocaleFromRequest(request) {
if (request?.headers) {
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage) {
const preferredLocales = acceptLanguage.split(',')
.map(lang => {
const [locale, priority = 'q=1.0'] = lang.trim().split(';');
return {
locale: locale.split('-')[0],
priority: parseFloat(priority.split('=')[1])
};
})
.sort((a, b) => b.priority - a.priority);
for (const { locale } of preferredLocales) {
if (translations.isLocaleSupported(locale)) {
return locale;
}
}
}
}
return DEFAULT_LOCALE.code;
}
};
export default translations;

View file

@ -1,33 +0,0 @@
import { createI18n } from 'vue-i18n'
import {DEFAULT_LOCALE, LOCALE_STORAGE_LOCALE_KEY, SUPPORTED_LOCALES} from "@/config/index.js";
import {loadAllLocaleMessages} from "@/core/helpers/i18n-utils.js";
const savedLocale = localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY)
const currentLocale = savedLocale && SUPPORTED_LOCALES.some(locale => locale.code === savedLocale)
? savedLocale
: DEFAULT_LOCALE
if (!savedLocale) {
localStorage.setItem(LOCALE_STORAGE_LOCALE_KEY, DEFAULT_LOCALE)
}
const i18n = createI18n({
locale: currentLocale,
fallbackLocale: DEFAULT_LOCALE,
allowComposition: true,
legacy: false,
globalInjection: true,
messages: {}
})
export async function setupI18n() {
const messages = await loadAllLocaleMessages(SUPPORTED_LOCALES)
Object.keys(messages).forEach(locale => {
i18n.global.setLocaleMessage(locale, messages[locale])
})
return i18n
}
export default i18n

View file

@ -0,0 +1,31 @@
---
import {useRefresh} from "@/composables/auth";
import {useCompanyInfo} from "@/composables/company";
import {useLanguages} from "@/composables/languages/index.js";
import BaseHeader from "@/components/base/header/base-header.vue";
import BaseFooter from "@/components/base/base-footer.vue";
document.addEventListener('DOMContentLoaded', async () => {
const { refresh } = useRefresh();
const { getCompanyInfo } = useCompanyInfo();
const { getLanguages } = useLanguages();
await refresh();
await getCompanyInfo();
await getLanguages();
setInterval(async () => {
await refresh();
}, 600000);
});
---
<div>
<BaseHeader client:load />
<slot />
<BaseFooter client:load />
</div>
<style scoped>
</style>

View file

@ -1,5 +1,5 @@
---
import DefaultLayout from '../layouts/default-layout.astro';
---
<html lang="en">
@ -11,6 +11,8 @@
<title>Astro</title>
</head>
<body>
<h1>Astro</h1>
<DefaultLayout>
<h1>Astro</h1>
</DefaultLayout>
</body>
</html>

View file

@ -0,0 +1,61 @@
import { createI18n } from 'vue-i18n'
import { DEFAULT_LOCALE, LOCALE_STORAGE_LOCALE_KEY, SUPPORTED_LOCALES } from "../config/index.js";
import { loadAllLocaleMessages } from "../core/helpers/i18n-utils.js";
export async function setupI18n() {
const i18n = createI18n({
locale: DEFAULT_LOCALE,
fallbackLocale: DEFAULT_LOCALE,
messages: {},
allowComposition: true,
legacy: false,
globalInjection: true,
});
const messages = await loadAllLocaleMessages(SUPPORTED_LOCALES);
Object.keys(messages).forEach(locale => {
i18n.global.setLocaleMessage(locale, messages[locale]);
});
if (typeof window !== 'undefined') {
const savedLocale = localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY);
const validLocale = savedLocale &&
SUPPORTED_LOCALES.some(locale => locale.code === savedLocale)
? savedLocale
: DEFAULT_LOCALE;
i18n.global.locale.value = validLocale;
if (!savedLocale) {
localStorage.setItem(LOCALE_STORAGE_LOCALE_KEY, DEFAULT_LOCALE);
}
}
return i18n;
}
export function getCurrentLocale(request) {
if (typeof window === 'undefined' && request) {
const acceptLanguageHeader = request.headers.get('accept-language');
if (acceptLanguageHeader) {
const preferredLocale = acceptLanguageHeader.split(',')[0].trim().split('-')[0];
const supportedLocale = SUPPORTED_LOCALES.find(locale =>
locale.code.startsWith(preferredLocale)
);
if (supportedLocale) {
return supportedLocale.code;
}
}
return DEFAULT_LOCALE;
}
if (typeof window !== 'undefined') {
const savedLocale = localStorage.getItem(LOCALE_STORAGE_LOCALE_KEY);
return savedLocale && SUPPORTED_LOCALES.some(locale => locale.code === savedLocale)
? savedLocale
: DEFAULT_LOCALE;
}
return DEFAULT_LOCALE;
}

View file

@ -0,0 +1,10 @@
import { setupI18n } from './i18n.js'
import { pinia } from "../stores/index.js";
export default function (app) {
app.use(pinia)
setupI18n().then(i18n => {
app.use(i18n)
})
}

View file

@ -0,0 +1,14 @@
import {defineStore} from "pinia";
import {ref} from "vue";
export const useCartStore = defineStore('cart', () => {
const currentOrder = ref({});
const setCurrentOrders = (order) => {
currentOrder.value = order
};
return {
currentOrder,
setCurrentOrders
}
})

View file

@ -0,0 +1,14 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCompanyStore = defineStore('company', () => {
const companyInfo = ref([]);
const setCompanyInfo = (payload) => {
companyInfo.value = payload
};
return {
companyInfo,
setCompanyInfo
}
})

View file

@ -0,0 +1,3 @@
import { createPinia } from 'pinia'
export const pinia = createPinia()

View file

@ -0,0 +1,14 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useLanguageStore = defineStore('language', () => {
const languages = ref([]);
const setLanguages = (payload) => {
languages.value = payload
};
return {
languages,
setLanguages
}
})

View file

@ -0,0 +1,11 @@
import {defineStore} from "pinia";
import {ref} from "vue";
export const useUserStore = defineStore('user', () => {
const user = ref(null);
const setUser = (payload) => {
user.value = payload.user
}
return { user, setUser }
})

View file

@ -0,0 +1,14 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useWishlistStore = defineStore('wishlist', () => {
const wishlist = ref({});
const setWishlist = (order) => {
wishlist.value = order
};
return {
wishlist,
setWishlist
}
})