847 lines
24 KiB
Vue
847 lines
24 KiB
Vue
<template>
|
||
<q-page v-if="canUpdateLanguage" class="q-pa-md translation-page">
|
||
<div class="translation-toolbar sticky-toolbar">
|
||
<div class="row q-col-gutter-sm items-end q-mb-md">
|
||
<div class="col-12 col-md-4">
|
||
<q-input
|
||
v-model="filters.q"
|
||
dense
|
||
outlined
|
||
clearable
|
||
label="Kelime ara"
|
||
/>
|
||
</div>
|
||
<div class="col-auto">
|
||
<q-btn color="primary" icon="search" label="Getir" @click="loadRows" />
|
||
</div>
|
||
<div class="col-auto">
|
||
<q-btn
|
||
color="secondary"
|
||
icon="sync"
|
||
label="YENİ KELİMELERİ GETİR"
|
||
:loading="store.saving"
|
||
@click="syncSources"
|
||
/>
|
||
</div>
|
||
<div class="col-auto">
|
||
<q-toggle v-model="autoTranslate" dense color="primary" label="Oto Çeviri" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row q-gutter-sm q-mb-sm">
|
||
<q-btn
|
||
color="accent"
|
||
icon="g_translate"
|
||
label="Seçilenleri Çevir"
|
||
:disable="selectedKeys.length === 0"
|
||
:loading="store.saving"
|
||
@click="translateSelectedRows"
|
||
/>
|
||
<q-btn
|
||
color="secondary"
|
||
icon="done_all"
|
||
label="Seçilenleri Onayla"
|
||
:disable="selectedKeys.length === 0"
|
||
:loading="store.saving"
|
||
@click="bulkApproveSelected"
|
||
/>
|
||
<q-btn
|
||
color="primary"
|
||
icon="save"
|
||
label="Seçilenleri Toplu Güncelle"
|
||
:disable="selectedKeys.length === 0"
|
||
:loading="store.saving"
|
||
@click="bulkSaveSelected"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<q-table
|
||
ref="tableRef"
|
||
class="translation-table"
|
||
flat
|
||
bordered
|
||
virtual-scroll
|
||
:virtual-scroll-sticky-size-start="56"
|
||
row-key="t_key"
|
||
:loading="store.loading || store.saving"
|
||
:rows="pivotRows"
|
||
:columns="columns"
|
||
:rows-per-page-options="[0]"
|
||
v-model:pagination="tablePagination"
|
||
hide-bottom
|
||
@virtual-scroll="onVirtualScroll"
|
||
>
|
||
<template #body-cell-actions="props">
|
||
<q-td :props="props">
|
||
<q-btn
|
||
dense
|
||
color="primary"
|
||
icon="save"
|
||
label="Güncelle"
|
||
:disable="!rowHasChanges(props.row.t_key)"
|
||
:loading="store.saving"
|
||
@click="saveRow(props.row.t_key)"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-select="props">
|
||
<q-td :props="props">
|
||
<q-checkbox
|
||
dense
|
||
:model-value="selectedKeys.includes(props.row.t_key)"
|
||
@update:model-value="(v) => toggleSelected(props.row.t_key, v)"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-source_text_tr="props">
|
||
<q-td :props="props" :class="cellClass(props.row.t_key, 'source_text_tr')">
|
||
<div class="source-text-label" :title="rowDraft(props.row.t_key).source_text_tr">
|
||
{{ rowDraft(props.row.t_key).source_text_tr }}
|
||
</div>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-source_type="props">
|
||
<q-td :props="props" :class="cellClass(props.row.t_key, 'source_type')">
|
||
<q-select
|
||
v-model="rowDraft(props.row.t_key).source_type"
|
||
dense
|
||
outlined
|
||
emit-value
|
||
map-options
|
||
:options="sourceTypeOptions"
|
||
@update:model-value="() => queueAutoSave(props.row.t_key)"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-en="props">
|
||
<q-td :props="props" :class="cellClass(props.row.t_key, 'en')">
|
||
<q-input
|
||
v-model="rowDraft(props.row.t_key).en"
|
||
type="textarea"
|
||
autogrow
|
||
:max-rows="8"
|
||
outlined
|
||
@blur="queueAutoSave(props.row.t_key)"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-de="props">
|
||
<q-td :props="props" :class="cellClass(props.row.t_key, 'de')">
|
||
<q-input
|
||
v-model="rowDraft(props.row.t_key).de"
|
||
type="textarea"
|
||
autogrow
|
||
:max-rows="8"
|
||
outlined
|
||
@blur="queueAutoSave(props.row.t_key)"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-es="props">
|
||
<q-td :props="props" :class="cellClass(props.row.t_key, 'es')">
|
||
<q-input
|
||
v-model="rowDraft(props.row.t_key).es"
|
||
type="textarea"
|
||
autogrow
|
||
:max-rows="8"
|
||
outlined
|
||
@blur="queueAutoSave(props.row.t_key)"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-it="props">
|
||
<q-td :props="props" :class="cellClass(props.row.t_key, 'it')">
|
||
<q-input
|
||
v-model="rowDraft(props.row.t_key).it"
|
||
type="textarea"
|
||
autogrow
|
||
:max-rows="8"
|
||
outlined
|
||
@blur="queueAutoSave(props.row.t_key)"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-ru="props">
|
||
<q-td :props="props" :class="cellClass(props.row.t_key, 'ru')">
|
||
<q-input
|
||
v-model="rowDraft(props.row.t_key).ru"
|
||
type="textarea"
|
||
autogrow
|
||
:max-rows="8"
|
||
outlined
|
||
@blur="queueAutoSave(props.row.t_key)"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
|
||
<template #body-cell-ar="props">
|
||
<q-td :props="props" :class="cellClass(props.row.t_key, 'ar')">
|
||
<q-input
|
||
v-model="rowDraft(props.row.t_key).ar"
|
||
type="textarea"
|
||
autogrow
|
||
:max-rows="8"
|
||
outlined
|
||
@blur="queueAutoSave(props.row.t_key)"
|
||
/>
|
||
</q-td>
|
||
</template>
|
||
</q-table>
|
||
</q-page>
|
||
|
||
<q-page v-else class="q-pa-md flex flex-center">
|
||
<div class="text-negative text-subtitle1">
|
||
Bu modüle erişim yetkiniz yok.
|
||
</div>
|
||
</q-page>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||
import { useQuasar } from 'quasar'
|
||
import { usePermission } from 'src/composables/usePermission'
|
||
import { useTranslationStore } from 'src/stores/translationStore'
|
||
|
||
const $q = useQuasar()
|
||
const store = useTranslationStore()
|
||
const { canUpdate } = usePermission()
|
||
const canUpdateLanguage = canUpdate('language')
|
||
|
||
const filters = ref({
|
||
q: ''
|
||
})
|
||
const autoTranslate = ref(false)
|
||
const tableRef = ref(null)
|
||
const FETCH_LIMIT = 1400
|
||
const loadedOffset = ref(0)
|
||
const hasMoreRows = ref(true)
|
||
const loadingMore = ref(false)
|
||
const tablePagination = ref({
|
||
page: 1,
|
||
rowsPerPage: 0,
|
||
sortBy: 'source_text_tr',
|
||
descending: false
|
||
})
|
||
let filterReloadTimer = null
|
||
|
||
const sourceTypeOptions = [
|
||
{ label: 'dummy', value: 'dummy' },
|
||
{ label: 'postgre', value: 'postgre' },
|
||
{ label: 'mssql', value: 'mssql' }
|
||
]
|
||
|
||
const columns = [
|
||
{ name: 'actions', label: 'Güncelle', field: 'actions', align: 'left' },
|
||
{ name: 'select', label: 'Seç', field: 'select', align: 'left' },
|
||
{ name: 'source_text_tr', label: 'Türkçe Metin', field: 'source_text_tr', align: 'left', style: 'min-width: 340px' },
|
||
{ name: 'source_type', label: 'Kaynak', field: 'source_type', align: 'left', style: 'min-width: 140px' },
|
||
{ name: 'en', label: 'İngilizce', field: 'en', align: 'left', style: 'min-width: 220px' },
|
||
{ name: 'de', label: 'Almanca', field: 'de', align: 'left', style: 'min-width: 220px' },
|
||
{ name: 'es', label: 'İspanyolca', field: 'es', align: 'left', style: 'min-width: 220px' },
|
||
{ name: 'it', label: 'İtalyanca', field: 'it', align: 'left', style: 'min-width: 220px' },
|
||
{ name: 'ru', label: 'Rusça', field: 'ru', align: 'left', style: 'min-width: 220px' },
|
||
{ name: 'ar', label: 'Arapça', field: 'ar', align: 'left', style: 'min-width: 220px' }
|
||
]
|
||
|
||
const draftByKey = ref({})
|
||
const originalByKey = ref({})
|
||
const selectedKeys = ref([])
|
||
const autoSaveTimers = new Map()
|
||
|
||
const pivotRows = computed(() => {
|
||
const byKey = new Map()
|
||
for (const row of store.rows) {
|
||
const key = row.t_key
|
||
if (!byKey.has(key)) {
|
||
byKey.set(key, {
|
||
t_key: key,
|
||
source_text_tr: '',
|
||
source_type: 'dummy',
|
||
en: '',
|
||
de: '',
|
||
es: '',
|
||
it: '',
|
||
ru: '',
|
||
ar: '',
|
||
langs: {}
|
||
})
|
||
}
|
||
|
||
const target = byKey.get(key)
|
||
target.langs[row.lang_code] = {
|
||
id: row.id,
|
||
status: row.status,
|
||
is_manual: row.is_manual
|
||
}
|
||
|
||
if (row.lang_code === 'tr') {
|
||
target.source_text_tr = row.translated_text || row.source_text_tr || ''
|
||
target.source_type = row.source_type || 'dummy'
|
||
} else if (row.lang_code === 'en') {
|
||
target.en = row.translated_text || ''
|
||
} else if (row.lang_code === 'de') {
|
||
target.de = row.translated_text || ''
|
||
} else if (row.lang_code === 'es') {
|
||
target.es = row.translated_text || ''
|
||
} else if (row.lang_code === 'it') {
|
||
target.it = row.translated_text || ''
|
||
} else if (row.lang_code === 'ru') {
|
||
target.ru = row.translated_text || ''
|
||
} else if (row.lang_code === 'ar') {
|
||
target.ar = row.translated_text || ''
|
||
}
|
||
}
|
||
|
||
return Array.from(byKey.values()).sort((a, b) => a.t_key.localeCompare(b.t_key))
|
||
})
|
||
|
||
function snapshotDrafts (options = {}) {
|
||
const preserveDirty = Boolean(options?.preserveDirty)
|
||
const draft = {}
|
||
const original = {}
|
||
for (const row of pivotRows.value) {
|
||
const existingDraft = draftByKey.value[row.t_key]
|
||
const existingOriginal = originalByKey.value[row.t_key]
|
||
const keepExisting = preserveDirty &&
|
||
existingDraft &&
|
||
existingOriginal &&
|
||
(
|
||
existingDraft.source_text_tr !== existingOriginal.source_text_tr ||
|
||
existingDraft.source_type !== existingOriginal.source_type ||
|
||
existingDraft.en !== existingOriginal.en ||
|
||
existingDraft.de !== existingOriginal.de ||
|
||
existingDraft.es !== existingOriginal.es ||
|
||
existingDraft.it !== existingOriginal.it ||
|
||
existingDraft.ru !== existingOriginal.ru ||
|
||
existingDraft.ar !== existingOriginal.ar
|
||
)
|
||
|
||
if (keepExisting) {
|
||
draft[row.t_key] = { ...existingDraft }
|
||
original[row.t_key] = { ...existingOriginal }
|
||
continue
|
||
}
|
||
|
||
draft[row.t_key] = {
|
||
source_text_tr: row.source_text_tr || '',
|
||
source_type: row.source_type || 'dummy',
|
||
en: row.en || '',
|
||
de: row.de || '',
|
||
es: row.es || '',
|
||
it: row.it || '',
|
||
ru: row.ru || '',
|
||
ar: row.ar || ''
|
||
}
|
||
original[row.t_key] = { ...draft[row.t_key] }
|
||
}
|
||
draftByKey.value = draft
|
||
originalByKey.value = original
|
||
selectedKeys.value = selectedKeys.value.filter(k => draft[k])
|
||
}
|
||
|
||
function rowDraft (key) {
|
||
if (!draftByKey.value[key]) {
|
||
draftByKey.value[key] = {
|
||
source_text_tr: '',
|
||
source_type: 'dummy',
|
||
en: '',
|
||
de: '',
|
||
es: '',
|
||
it: '',
|
||
ru: '',
|
||
ar: ''
|
||
}
|
||
}
|
||
return draftByKey.value[key]
|
||
}
|
||
|
||
function buildFilters () {
|
||
const query = String(filters.value.q || '').trim()
|
||
return {
|
||
q: query || undefined
|
||
}
|
||
}
|
||
|
||
function rowHasChanges (key) {
|
||
const draft = draftByKey.value[key]
|
||
const orig = originalByKey.value[key]
|
||
if (!draft || !orig) return false
|
||
return (
|
||
draft.source_text_tr !== orig.source_text_tr ||
|
||
draft.source_type !== orig.source_type ||
|
||
draft.en !== orig.en ||
|
||
draft.de !== orig.de ||
|
||
draft.es !== orig.es ||
|
||
draft.it !== orig.it ||
|
||
draft.ru !== orig.ru ||
|
||
draft.ar !== orig.ar
|
||
)
|
||
}
|
||
|
||
function isPending (key, lang) {
|
||
const row = pivotRows.value.find(r => r.t_key === key)
|
||
const meta = row?.langs?.[lang]
|
||
return meta?.status === 'pending'
|
||
}
|
||
|
||
function cellClass (key, field) {
|
||
const draft = draftByKey.value[key]
|
||
const orig = originalByKey.value[key]
|
||
if (!draft || !orig) return ''
|
||
|
||
if (draft[field] !== orig[field]) return 'cell-dirty'
|
||
|
||
if (field === 'en' && isPending(key, 'en')) return 'cell-new'
|
||
if (field === 'de' && isPending(key, 'de')) return 'cell-new'
|
||
if (field === 'es' && isPending(key, 'es')) return 'cell-new'
|
||
if (field === 'it' && isPending(key, 'it')) return 'cell-new'
|
||
if (field === 'ru' && isPending(key, 'ru')) return 'cell-new'
|
||
if (field === 'ar' && isPending(key, 'ar')) return 'cell-new'
|
||
if (field === 'source_text_tr' && isPending(key, 'tr')) return 'cell-new'
|
||
return ''
|
||
}
|
||
|
||
function toggleSelected (key, checked) {
|
||
if (checked) {
|
||
if (!selectedKeys.value.includes(key)) {
|
||
selectedKeys.value = [...selectedKeys.value, key]
|
||
}
|
||
return
|
||
}
|
||
selectedKeys.value = selectedKeys.value.filter(k => k !== key)
|
||
}
|
||
|
||
function queueAutoSave (key) {
|
||
if (!key) return
|
||
const existing = autoSaveTimers.get(key)
|
||
if (existing) {
|
||
clearTimeout(existing)
|
||
}
|
||
const timer = setTimeout(() => {
|
||
autoSaveTimers.delete(key)
|
||
if (rowHasChanges(key)) {
|
||
void saveRow(key)
|
||
}
|
||
}, 250)
|
||
autoSaveTimers.set(key, timer)
|
||
}
|
||
|
||
async function fetchRowsChunk (append = false) {
|
||
const params = {
|
||
...buildFilters(),
|
||
limit: FETCH_LIMIT,
|
||
offset: append ? loadedOffset.value : 0
|
||
}
|
||
|
||
await store.fetchRows(params, { append })
|
||
const incomingCount = Number(store.count) || 0
|
||
|
||
if (append) {
|
||
loadedOffset.value += incomingCount
|
||
} else {
|
||
loadedOffset.value = incomingCount
|
||
}
|
||
hasMoreRows.value = incomingCount === FETCH_LIMIT
|
||
snapshotDrafts({ preserveDirty: append })
|
||
}
|
||
|
||
async function loadRows () {
|
||
try {
|
||
loadedOffset.value = 0
|
||
hasMoreRows.value = true
|
||
await fetchRowsChunk(false)
|
||
} catch (err) {
|
||
console.error('[translation-sync][ui] loadRows:error', {
|
||
message: err?.message || 'Çeviri satırları yüklenemedi'
|
||
})
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: err?.message || 'Çeviri satırları yüklenemedi'
|
||
})
|
||
}
|
||
}
|
||
|
||
async function loadMoreRows () {
|
||
if (!hasMoreRows.value || loadingMore.value || store.loading || store.saving) return
|
||
loadingMore.value = true
|
||
try {
|
||
await fetchRowsChunk(true)
|
||
} finally {
|
||
loadingMore.value = false
|
||
}
|
||
}
|
||
|
||
async function ensureEnoughVisibleRows (minRows = 120, maxBatches = 4) {
|
||
let guard = 0
|
||
while (hasMoreRows.value && pivotRows.value.length < minRows && guard < maxBatches) {
|
||
await loadMoreRows()
|
||
guard++
|
||
}
|
||
}
|
||
|
||
function onVirtualScroll (details) {
|
||
const to = Number(details?.to || 0)
|
||
if (!Number.isFinite(to)) return
|
||
if (to >= pivotRows.value.length - 15) {
|
||
void loadMoreRows()
|
||
}
|
||
}
|
||
|
||
function scheduleFilterReload () {
|
||
if (filterReloadTimer) {
|
||
clearTimeout(filterReloadTimer)
|
||
}
|
||
filterReloadTimer = setTimeout(() => {
|
||
filterReloadTimer = null
|
||
void loadRows()
|
||
}, 350)
|
||
}
|
||
|
||
async function ensureMissingLangRows (key, draft, langs) {
|
||
const missingLangs = []
|
||
if (!langs.en && String(draft.en || '').trim() !== '') missingLangs.push('en')
|
||
if (!langs.de && String(draft.de || '').trim() !== '') missingLangs.push('de')
|
||
if (!langs.es && String(draft.es || '').trim() !== '') missingLangs.push('es')
|
||
if (!langs.it && String(draft.it || '').trim() !== '') missingLangs.push('it')
|
||
if (!langs.ru && String(draft.ru || '').trim() !== '') missingLangs.push('ru')
|
||
if (!langs.ar && String(draft.ar || '').trim() !== '') missingLangs.push('ar')
|
||
if (missingLangs.length === 0) return false
|
||
|
||
await store.upsertMissing([
|
||
{
|
||
t_key: key,
|
||
source_text_tr: draft.source_text_tr || key
|
||
}
|
||
], missingLangs)
|
||
return true
|
||
}
|
||
|
||
function buildRowUpdates (row, draft, original, approveStatus = 'approved') {
|
||
const items = []
|
||
const langs = row.langs || {}
|
||
const sourceTypeChanged = draft.source_type !== original.source_type
|
||
|
||
if (langs.tr?.id && (draft.source_text_tr !== original.source_text_tr || sourceTypeChanged)) {
|
||
items.push({
|
||
id: langs.tr.id,
|
||
source_text_tr: draft.source_text_tr,
|
||
translated_text: draft.source_text_tr,
|
||
source_type: draft.source_type,
|
||
status: approveStatus,
|
||
is_manual: true
|
||
})
|
||
}
|
||
if (langs.en?.id && (draft.en !== original.en || sourceTypeChanged)) {
|
||
items.push({
|
||
id: langs.en.id,
|
||
translated_text: draft.en,
|
||
source_type: draft.source_type,
|
||
status: approveStatus,
|
||
is_manual: true
|
||
})
|
||
}
|
||
if (langs.de?.id && (draft.de !== original.de || sourceTypeChanged)) {
|
||
items.push({
|
||
id: langs.de.id,
|
||
translated_text: draft.de,
|
||
source_type: draft.source_type,
|
||
status: approveStatus,
|
||
is_manual: true
|
||
})
|
||
}
|
||
if (langs.es?.id && (draft.es !== original.es || sourceTypeChanged)) {
|
||
items.push({
|
||
id: langs.es.id,
|
||
translated_text: draft.es,
|
||
source_type: draft.source_type,
|
||
status: approveStatus,
|
||
is_manual: true
|
||
})
|
||
}
|
||
if (langs.it?.id && (draft.it !== original.it || sourceTypeChanged)) {
|
||
items.push({
|
||
id: langs.it.id,
|
||
translated_text: draft.it,
|
||
source_type: draft.source_type,
|
||
status: approveStatus,
|
||
is_manual: true
|
||
})
|
||
}
|
||
if (langs.ru?.id && (draft.ru !== original.ru || sourceTypeChanged)) {
|
||
items.push({
|
||
id: langs.ru.id,
|
||
translated_text: draft.ru,
|
||
source_type: draft.source_type,
|
||
status: approveStatus,
|
||
is_manual: true
|
||
})
|
||
}
|
||
if (langs.ar?.id && (draft.ar !== original.ar || sourceTypeChanged)) {
|
||
items.push({
|
||
id: langs.ar.id,
|
||
translated_text: draft.ar,
|
||
source_type: draft.source_type,
|
||
status: approveStatus,
|
||
is_manual: true
|
||
})
|
||
}
|
||
return items
|
||
}
|
||
|
||
async function saveRow (key) {
|
||
const row = pivotRows.value.find(r => r.t_key === key)
|
||
const draft = draftByKey.value[key]
|
||
const original = originalByKey.value[key]
|
||
if (!row || !draft || !original || !rowHasChanges(key)) return
|
||
|
||
try {
|
||
const insertedMissing = await ensureMissingLangRows(key, draft, row.langs || {})
|
||
if (insertedMissing) {
|
||
await loadRows()
|
||
}
|
||
|
||
const refreshed = pivotRows.value.find(r => r.t_key === key)
|
||
if (!refreshed) return
|
||
const refreshDraft = draftByKey.value[key]
|
||
const refreshOriginal = originalByKey.value[key]
|
||
const items = buildRowUpdates(refreshed, refreshDraft, refreshOriginal)
|
||
if (items.length > 0) {
|
||
await store.bulkUpdate(items)
|
||
}
|
||
|
||
await loadRows()
|
||
$q.notify({ type: 'positive', message: 'Satır güncellendi' })
|
||
} catch (err) {
|
||
$q.notify({ type: 'negative', message: err?.message || 'Güncelleme hatası' })
|
||
}
|
||
}
|
||
|
||
async function bulkApproveSelected () {
|
||
try {
|
||
const ids = []
|
||
for (const key of selectedKeys.value) {
|
||
const row = pivotRows.value.find(r => r.t_key === key)
|
||
if (!row) continue
|
||
for (const lang of ['tr', 'en', 'de', 'es', 'it', 'ru', 'ar']) {
|
||
const meta = row.langs?.[lang]
|
||
if (meta?.id && meta?.status === 'pending') {
|
||
ids.push(meta.id)
|
||
}
|
||
}
|
||
}
|
||
const unique = Array.from(new Set(ids))
|
||
if (unique.length === 0) {
|
||
$q.notify({ type: 'warning', message: 'Onaylanacak pending kayıt bulunamadı' })
|
||
return
|
||
}
|
||
await store.bulkApprove(unique)
|
||
await loadRows()
|
||
$q.notify({ type: 'positive', message: `${unique.length} kayıt onaylandı` })
|
||
} catch (err) {
|
||
$q.notify({ type: 'negative', message: err?.message || 'Toplu onay hatası' })
|
||
}
|
||
}
|
||
|
||
async function translateSelectedRows () {
|
||
try {
|
||
const keys = Array.from(new Set(selectedKeys.value.filter(Boolean)))
|
||
if (keys.length === 0) {
|
||
$q.notify({ type: 'warning', message: 'Çevrilecek seçim bulunamadı' })
|
||
return
|
||
}
|
||
|
||
const response = await store.translateSelected({
|
||
t_keys: keys,
|
||
languages: ['en', 'de', 'it', 'es', 'ru', 'ar'],
|
||
limit: Math.min(50000, keys.length * 6)
|
||
})
|
||
|
||
const translated = Number(response?.translated_count || 0)
|
||
const traceId = response?.trace_id || null
|
||
|
||
await loadRows()
|
||
$q.notify({
|
||
type: 'positive',
|
||
message: `Seçilenler çevrildi: ${translated}${traceId ? ` | Trace: ${traceId}` : ''}`
|
||
})
|
||
} catch (err) {
|
||
$q.notify({ type: 'negative', message: err?.message || 'Seçili çeviri işlemi başarısız' })
|
||
}
|
||
}
|
||
|
||
async function bulkSaveSelected () {
|
||
try {
|
||
const items = []
|
||
for (const key of selectedKeys.value) {
|
||
const row = pivotRows.value.find(r => r.t_key === key)
|
||
const draft = draftByKey.value[key]
|
||
const original = originalByKey.value[key]
|
||
if (!row || !draft || !original) continue
|
||
if (!rowHasChanges(key)) continue
|
||
|
||
const insertedMissing = await ensureMissingLangRows(key, draft, row.langs || {})
|
||
if (insertedMissing) {
|
||
await loadRows()
|
||
}
|
||
|
||
const refreshed = pivotRows.value.find(r => r.t_key === key)
|
||
if (!refreshed) continue
|
||
const refreshDraft = draftByKey.value[key]
|
||
const refreshOriginal = originalByKey.value[key]
|
||
items.push(...buildRowUpdates(refreshed, refreshDraft, refreshOriginal))
|
||
}
|
||
|
||
if (items.length === 0) {
|
||
$q.notify({ type: 'warning', message: 'Toplu güncellenecek değişiklik yok' })
|
||
return
|
||
}
|
||
|
||
await store.bulkUpdate(items)
|
||
await loadRows()
|
||
$q.notify({ type: 'positive', message: `${items.length} kayıt toplu güncellendi` })
|
||
} catch (err) {
|
||
$q.notify({ type: 'negative', message: err?.message || 'Toplu güncelleme hatası' })
|
||
}
|
||
}
|
||
|
||
async function syncSources () {
|
||
const startedAt = Date.now()
|
||
const beforeCount = pivotRows.value.length
|
||
console.info('[translation-sync][ui] button:click', {
|
||
at: new Date(startedAt).toISOString(),
|
||
auto_translate: autoTranslate.value,
|
||
only_new: true,
|
||
before_row_count: beforeCount
|
||
})
|
||
try {
|
||
const response = await store.syncSources({
|
||
auto_translate: autoTranslate.value,
|
||
languages: ['en', 'de', 'it', 'es', 'ru', 'ar'],
|
||
limit: 1000,
|
||
only_new: true
|
||
})
|
||
const result = response?.result || response || {}
|
||
const traceId = result?.trace_id || response?.trace_id || null
|
||
console.info('[translation-sync][ui] sync:response', {
|
||
trace_id: traceId,
|
||
seed_count: result.seed_count || 0,
|
||
affected_count: result.affected_count || 0,
|
||
auto_translated: result.auto_translated || 0,
|
||
duration_ms: result.duration_ms || null
|
||
})
|
||
await loadRows()
|
||
const afterCount = pivotRows.value.length
|
||
console.info('[translation-sync][ui] chain:reload-complete', {
|
||
trace_id: traceId,
|
||
duration_ms: Date.now() - startedAt,
|
||
before_row_count: beforeCount,
|
||
after_row_count: afterCount,
|
||
delta_row_count: afterCount - beforeCount
|
||
})
|
||
$q.notify({
|
||
type: 'positive',
|
||
message: `Tarama tamamlandı. Seed: ${result.seed_count || 0}, Oto çeviri: ${result.auto_translated || 0}`
|
||
})
|
||
} catch (err) {
|
||
$q.notify({
|
||
type: 'negative',
|
||
message: err?.message || 'Kaynak tarama hatası'
|
||
})
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
void loadRows()
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
if (filterReloadTimer) {
|
||
clearTimeout(filterReloadTimer)
|
||
filterReloadTimer = null
|
||
}
|
||
})
|
||
|
||
watch(
|
||
() => filters.value.q,
|
||
() => { scheduleFilterReload() }
|
||
)
|
||
|
||
watch(
|
||
[() => tablePagination.value.sortBy, () => tablePagination.value.descending],
|
||
() => { void ensureEnoughVisibleRows(120, 4) }
|
||
)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.translation-page {
|
||
height: calc(100vh - 120px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.translation-toolbar {
|
||
background: #fff;
|
||
padding-top: 6px;
|
||
}
|
||
|
||
.sticky-toolbar {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 35;
|
||
}
|
||
|
||
.translation-table {
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.translation-table :deep(.q-table__middle) {
|
||
max-height: calc(100vh - 280px);
|
||
overflow: auto;
|
||
}
|
||
|
||
.translation-table :deep(.q-table thead tr th) {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 30;
|
||
background: #fff;
|
||
}
|
||
|
||
.translation-table :deep(.q-table tbody td) {
|
||
vertical-align: top;
|
||
padding: 6px;
|
||
}
|
||
|
||
.translation-table :deep(.q-field__native) {
|
||
line-height: 1.35;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.source-text-label {
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
line-height: 1.4;
|
||
max-height: 11.2em;
|
||
overflow: auto;
|
||
}
|
||
|
||
.cell-dirty {
|
||
background: #fff3cd;
|
||
}
|
||
|
||
.cell-new {
|
||
background: #d9f7e8;
|
||
}
|
||
</style>
|
||
|