fix: translations

This commit is contained in:
2026-04-02 11:41:44 +02:00
parent a7f69c854a
commit 551b5ef77d
5 changed files with 157 additions and 232 deletions

View File

@@ -41,7 +41,7 @@
}" color="primary" class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!"> }" color="primary" class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!">
Cancel Cancel
</UButton> </UButton>
<UButton color="primary" @click="saveDescription" <UButton color="primary" @click="productStore.saveProductDescription(productID, toLangId)"
class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!"> class="text-white bg-(--accent-blue-light) dark:bg-(--accent-blue-dark) px-12!">
Save Save
</UButton> </UButton>

View File

@@ -1,34 +1,34 @@
<template> <template>
<div class="flex flex-col"> <USelectMenu v-model="country" :items="countries" value-key="id"
<p class="text-sm">Country/Currency:</p> class="w-48 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm" :searchInput="false">
<USelectMenu v-model="country" :items="countries" <template #default>
class="w-44 bg-(--main-light) dark:bg-(--black) rounded-md hover:none! text-sm!" valueKey="id" <div class="flex flex-col items-start leading-tight">
:searchInput="false"> <span class="text-xs text-gray-400">
<template #default="{ modelValue }"> Country/Currency
<div class="flex items-center gap-1"> </span>
<span class="font-medium dark:text-white text-black whitespace-nowrap">{{ modelValue.name }} / {{ <span class="font-medium dark:text-white text-black">
currentCountry?.ps_currency.iso_code }}</span> {{ country?.name }} / {{ country?.ps_currency.iso_code }}
</div> </span>
</template> </div>
<template #item-leading="{ item }"> </template>
<div class="flex items-center rounded-md cursor-pointer transition-colors">
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span> <template #item-leading="{ item }">
</div> <div class="flex items-center gap-2 cursor-pointer">
</template> <span class="text-lg">{{ item.flag }}</span>
</USelectMenu> <span class="font-medium dark:text-white text-black">
</div> {{ item.name }}
</span>
</div>
</template>
</USelectMenu>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { countries, currentCountry, switchLocalization } from '@/router/langs' import { countries, currentCountry, switchLocalization } from '@/router/langs'
import { useRouter, useRoute } from 'vue-router'
import { useCookie } from '@/composable/useCookie' import { useCookie } from '@/composable/useCookie'
import { computed, watch } from 'vue' import { computed, watch } from 'vue'
const router = useRouter()
const route = useRoute()
const cookie = useCookie() const cookie = useCookie()
const country = computed({ const country = computed({
get() { get() {
return currentCountry.value return currentCountry.value

View File

@@ -1,24 +1,30 @@
<template> <template>
<div class="flex flex-col"> <USelectMenu v-model="locale" :items="langs" value-key="iso_code"
<p class="text-sm">Language:</p> class="w-48 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm" :searchInput="false">
<USelectMenu v-model="locale" :items="langs" <template #default>
class="w-40 bg-(--main-light) dark:bg-(--black) rounded-md shadow-sm hover:none!" valueKey="iso_code" <div class="flex items-center gap-2">
:searchInput="false"> <!-- <span class="text-lg">{{ selectedLang?.flag }}</span> -->
<template #default="{ modelValue }">
<div class="flex items-center gap-1"> <div class="flex flex-col leading-tight items-start">
<!-- <span class="text-md dark:text-white text-black">{{langs.find(x => x.iso_code == modelValue)?.flag}}</span> --> <span class="text-xs text-gray-400">
<span class="font-medium dark:text-white text-black">{{langs.find(x => x.iso_code == Language
modelValue)?.name}}</span> </span>
<span class="font-medium dark:text-white text-black">
{{ selectedLang?.name || 'Select language' }}
</span>
</div> </div>
</template> </div>
<template #item-leading="{ item }"> </template>
<div class="flex items-center rounded-md cursor-pointer transition-colors">
<!-- <span class="text-md ">{{ item.flag }}</span> --> <template #item-leading="{ item }">
<span class="ml-2 dark:text-white text-black font-medium">{{ item.name }}</span> <div class="flex items-center gap-2">
</div> <span class="text-lg">{{ item.flag }}</span>
</template> <span class="font-medium dark:text-white text-black">
</USelectMenu> {{ item.name }}
</div> </span>
</div>
</template>
</USelectMenu>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -31,6 +37,10 @@ import { i18n } from '@/plugins/02_i18n'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const selectedLang = computed(() =>
langs.find(item => item.iso_code === locale.value)
)
const cookie = useCookie() const cookie = useCookie()
const locale = computed({ const locale = computed({
get() { get() {

View File

@@ -1,19 +1,61 @@
<!-- <script setup lang="ts">
import TopBar from '@/components/TopBar.vue';
</script>
<template> <template>
<div class="h-screen grid grid-rows-[auto_1fr_auto]"> <div class="flex flex-1">
<main class="pt-20 pb-10"> <USidebar v-model:open="open" collapsible="icon" rail :ui="{
<TopBar /> container: 'h-full',
<div class=" px-4"> inner: 'bg-elevated/25 divide-transparent',
body: 'py-0'
}">
<template #header>
<UDropdownMenu :items="teamsItems" :content="{ align: 'start', collisionPadding: 12 }"
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
<UButton v-bind="selectedTeam" trailing-icon="i-lucide-chevrons-up-down" color="neutral" variant="ghost"
square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
trailingIcon: 'text-dimmed ms-auto'
}" />
</UDropdownMenu>
</template>
<template #default="{ state }">
<UNavigationMenu :key="state" :items="menuItems" orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }" />
</template>
<template #footer>
<UDropdownMenu :items="userItems" :content="{ align: 'center', collisionPadding: 12 }"
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
<UButton v-bind="user" :label="user?.name" trailing-icon="i-lucide-chevrons-up-down" color="neutral"
variant="ghost" square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
trailingIcon: 'text-dimmed ms-auto'
}" />
</UDropdownMenu>
</template>
</USidebar>
<div class="flex-1 flex flex-col">
<div class="h-(--ui-header-height) shrink-0 flex items-center justify-between px-4 border-b border-default">
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
@click="open = !open" />
<div class="flex items-center gap-12">
<div class="flex items-center gap-2">
<CountryCurrencySwitch />
<LangSwitch />
</div>
<div class="flex items-center gap-2">
<ThemeSwitch />
<button v-if="authStore.isAuthenticated" @click="authStore.logout()"
class="px-3 py-1.5 text-sm font-medium text-black dark:text-white hover:text-black dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600 rounded-lg transition-colors border border-(--border-light) dark:border-(--border-dark) whitespace-nowrap">
{{ $t('general.logout') }}
</button>
</div>
</div>
</div>
<div class="flex-1 p-4">
<slot /> <slot />
</div> </div>
</main> </div>
</div> </div>
</template> --> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
@@ -23,7 +65,7 @@ import { defineShortcuts, extractShortcuts } from '@nuxt/ui/runtime/composables/
import { LabelTrans, TopMenuItem } from '@/types' import { LabelTrans, TopMenuItem } from '@/types'
const open = ref(true) const open = ref(true)
const authStore = useAuthStore()
const colorMode = useColorMode() const colorMode = useColorMode()
const teams = ref([ const teams = ref([
@@ -113,6 +155,9 @@ function getItems(state: 'collapsed' | 'expanded') {
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { currentLang } from '@/router/langs' import { currentLang } from '@/router/langs'
import { useFetchJson } from '@/composable/useFetchJson' import { useFetchJson } from '@/composable/useFetchJson'
import { useAuthStore } from '@/stores/auth'
import CountryCurrencySwitch from '@/components/inner/CountryCurrencySwitch.vue'
import LangSwitch from '@/components/inner/LangSwitch.vue'
const router = useRouter() const router = useRouter()
@@ -246,49 +291,3 @@ const userItems = computed<DropdownMenuItem[][]>(() => [
defineShortcuts(extractShortcuts(teamsItems.value)) defineShortcuts(extractShortcuts(teamsItems.value))
</script> </script>
<template>
<div class="flex flex-1">
<USidebar v-model:open="open" collapsible="icon" rail :ui="{
container: 'h-full',
inner: 'bg-elevated/25 divide-transparent',
body: 'py-0'
}">
<template #header>
<UDropdownMenu :items="teamsItems" :content="{ align: 'start', collisionPadding: 12 }"
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
<UButton v-bind="selectedTeam" trailing-icon="i-lucide-chevrons-up-down" color="neutral" variant="ghost"
square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
trailingIcon: 'text-dimmed ms-auto'
}" />
</UDropdownMenu>
</template>
<template #default="{ state }">
<UNavigationMenu :key="state" :items="menuItems" orientation="vertical"
:ui="{ link: 'p-1.5 overflow-hidden' }" />
</template>
<template #footer>
<UDropdownMenu :items="userItems" :content="{ align: 'center', collisionPadding: 12 }"
:ui="{ content: 'w-(--reka-dropdown-menu-trigger-width) min-w-48' }">
<UButton v-bind="user" :label="user?.name" trailing-icon="i-lucide-chevrons-up-down" color="neutral"
variant="ghost" square class="w-full data-[state=open]:bg-elevated overflow-hidden" :ui="{
trailingIcon: 'text-dimmed ms-auto'
}" />
</UDropdownMenu>
</template>
</USidebar>
<div class="flex-1 flex flex-col">
<div class="h-(--ui-header-height) shrink-0 flex items-center px-4 border-b border-default">
<UButton icon="i-lucide-panel-left" color="neutral" variant="ghost" aria-label="Toggle sidebar"
@click="open = !open" />
</div>
<div class="flex-1 p-4">
<slot />
</div>
</div>
</div>
</template>

View File

@@ -45,6 +45,7 @@ export const useProductStore = defineStore('product', () => {
} }
} }
const translat = ref()
const settingStore = useSettingsStore() const settingStore = useSettingsStore()
async function translateProductDescription(productID: number, toLangId: number, model: string = 'Google') { async function translateProductDescription(productID: number, toLangId: number, model: string = 'Google') {
loading.value = true loading.value = true
@@ -53,6 +54,7 @@ export const useProductStore = defineStore('product', () => {
try { try {
const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-translation/translate-product-description?productID=${productID}&productFromLangID=${settingStore.shopDefaultLanguage}&productToLangID=${toLangId}&model=${model}`) const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-translation/translate-product-description?productID=${productID}&productFromLangID=${settingStore.shopDefaultLanguage}&productToLangID=${toLangId}&model=${model}`)
productDescription.value = response.items productDescription.value = response.items
saveProductDescription(productID, toLangId)
return response.items return response.items
} catch (e: any) { } catch (e: any) {
error.value = e?.message || 'Failed to translate product description' error.value = e?.message || 'Failed to translate product description'
@@ -62,16 +64,46 @@ export const useProductStore = defineStore('product', () => {
} }
} }
function fixHtml(html: string) {
return html
// 1. fix img
.replace(/<img([^>]*?)>/g, '<img$1 />')
function stripHtml(html: string) { // 2. escape text only
const div = document.createElement('div') .replace(/>([^<]+)</g, (match, text) => {
div.innerHTML = html const escaped = text
return div.textContent || div.innerText || '' .replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
return `>${escaped}<`
})
} }
async function saveProductDescription(productID?: number, langId?: number) { function fixAll(obj: any): any {
if (typeof obj === 'string') {
return fixHtml(obj)
}
if (Array.isArray(obj)) {
return obj.map(fixAll)
}
if (typeof obj === 'object' && obj !== null) {
const result: any = {}
for (const [key, value] of Object.entries(obj)) {
result[key] = fixAll(value)
}
return result
}
return obj
}
async function saveProductDescription(productID?: number, langId?: number | null) {
const id = productID || 1 const id = productID || 1
const lang = langId || 1 const lang = langId || 1
try { try {
const data = await useFetchJson( const data = await useFetchJson(
`/api/v1/restricted/product-translation/save-product-description?productID=${id}&productLangID=${lang}`, `/api/v1/restricted/product-translation/save-product-description?productID=${id}&productLangID=${lang}`,
@@ -81,14 +113,14 @@ export const useProductStore = defineStore('product', () => {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
name: stripHtml(productDescription.value?.name || ''), name: productDescription.value?.name || '',
description: stripHtml(productDescription.value?.description || ''), description: productDescription.value?.description || '',
description_short: stripHtml(productDescription.value?.description_short || ''), description_short: productDescription.value?.description_short || '',
meta_title: stripHtml(productDescription.value?.meta_title || ''), meta_title: productDescription.value?.meta_title || '',
meta_description: stripHtml(productDescription.value?.meta_description || ''), meta_description: productDescription.value?.meta_description || '',
available_now: stripHtml(productDescription.value?.available_now || ''), available_now: productDescription.value?.available_now || '',
available_later: stripHtml(productDescription.value?.available_later || ''), available_later: productDescription.value?.available_later || '',
usage: stripHtml(productDescription.value?.usage || ''), usage: productDescription.value?.usage || '',
// delivery_in_stock: stripHtml(productDescription.value?.delivery_in_stock || '') // delivery_in_stock: stripHtml(productDescription.value?.delivery_in_stock || '')
}) })
} }
@@ -103,126 +135,10 @@ export const useProductStore = defineStore('product', () => {
productDescription, productDescription,
loading, loading,
error, error,
translat,
translateProductDescription, translateProductDescription,
getProductDescription, getProductDescription,
saveProductDescription saveProductDescription
} }
}) })
// import { defineStore } from 'pinia'
// import { ref } from 'vue'
// import { useFetchJson } from '@/composable/useFetchJson'
// import type { ProductDescription } from '@/types/product'
// import { useSettingsStore } from './settings'
// export interface Product {
// id: number
// image: string
// name: string
// code: string
// inStock: boolean
// priceFrom: number
// priceTo: number
// count: number
// description?: string
// howToUse?: string
// productDetails?: string
// }
// export interface ProductResponse {
// items: Product[]
// items_count: number
// }
// export const useProductStore = defineStore('product', () => {
// const productDescription = ref()
// const currentProduct = ref<Product | null>(null)
// const loading = ref(false)
// const error = ref<string | null>(null)
// async function getProductDescription(langId = 1, productID: number) {
// loading.value = true
// error.value = null
// try {
// const response = await useFetchJson<ProductDescription>(
// `/api/v1/restricted/product-translation/get-product-description?productID=${productID}&productLangID=${langId}`
// )
// productDescription.value = response.items
// console.log(productDescription, 'dfsfsdf');
// } catch (e: unknown) {
// error.value = e instanceof Error ? e.message : 'Failed to load product description'
// } finally {
// loading.value = false
// }
// }
// function stripHtml(html: string) {
// const div = document.createElement('div')
// div.innerHTML = html
// return div.textContent || div.innerText || ''
// }
// async function saveProductDescription(productID?: number, langId?: number) {
// const id = productID || 1
// const lang = langId || 1
// try {
// const data = await useFetchJson(
// `/api/v1/restricted/product-translation/save-product-description?productID=${id}&productLangID=${lang}`,
// {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify({
// name: stripHtml(productDescription.value?.name || ''),
// description: stripHtml(productDescription.value?.description || ''),
// description_short: stripHtml(productDescription.value?.description_short || ''),
// meta_title: stripHtml(productDescription.value?.meta_title || ''),
// meta_description: stripHtml(productDescription.value?.meta_description || ''),
// available_now: stripHtml(productDescription.value?.available_now || ''),
// available_later: stripHtml(productDescription.value?.available_later || ''),
// usage: stripHtml(productDescription.value?.usage || '')
// })
// }
// )
// return data
// } catch (e) {
// console.error(e)
// }
// }
// const defaultLangId = ref(1)
// async function translateProductDescription(productID: number, fromLangId: number, defaultLangId: number, model: string = 'OpenAI') {
// loading.value = true
// error.value = null
// try {
// const response = await useFetchJson<ProductDescription>(`/api/v1/restricted/product-translation/translate-product-description?productID=${productID}&productFromLangID=${fromLangId}&productToLangID=${defaultLangId}&model=${model}`)
// productDescription.value = response.items
// return response.items
// } catch (e: any) {
// error.value = e?.message || 'Failed to translate product description'
// console.error('Failed to translate product description:', e)
// } finally {
// loading.value = false
// }
// }
// function clearCurrentProduct() {
// currentProduct.value = null
// }
// return {
// productDescription,
// currentProduct,
// loading,
// error,
// getProductDescription,
// clearCurrentProduct,
// saveProductDescription,
// translateProductDescription,
// }
// })