@@ -122,10 +207,20 @@ export default function PublicTranslationReview() {
- {t('translations.translationTexts')} ({translation.texts.length})
+ {t('translations.translationTexts')}
-
- {translation.texts.map((text, idx) => (
-
-
-
- {text.language_label || text.language_code}
-
-
({text.language_code})
+
+ {Object.entries(textsByLanguage).map(([langCode, options]) => {
+ const langLabel = options[0]?.language_label || langCode
+ const hasSelected = options.some(isTextSelected)
+ return (
+
+
+
+ {langLabel}
+ ({langCode})
+
+ — {options.length} {options.length === 1 ? t('translations.option') : t('translations.options')}
+
+
+ {isPendingReview && (
+
+ )}
+
+
+ {options.map((text) => {
+ const textId = text.Id || text.id
+ const isSelected = isTextSelected(text)
+ // When approved, show only the selected option prominently; others are dimmed
+ const isDimmed = isApproved && hasSelected && !isSelected
+ return (
+
+
+
+
+
+ {t('translations.optionLabel')} {text.option_number || 1}
+
+ {isSelected && (
+
+
+ {t('translations.selected')}
+
+ )}
+
+
{text.content}
+
+
+ {/* Copy button — always available, especially useful for approved */}
+ {(isApproved || isSelected) && (
+
+ )}
+ {/* Select button — only when pending review */}
+ {isPendingReview && (
+
+ )}
+
+
+
+ )
+ })}
+
+
+ {/* Inline suggestion form for this language */}
+ {suggestingLang === langCode && (
+
+
{t('translations.suggestForLang')} {langLabel}
+
+ )}
-
{text.content}
-
- ))}
+ )
+ })}
)}
- {/* Review Actions */}
- {translation.status === 'pending_review' && (
+ {/* Review Actions — only pending_review */}
+ {isPendingReview && (
{t('review.yourReview')}
@@ -253,17 +457,63 @@ export default function PublicTranslationReview() {
)}
- {/* Already reviewed */}
- {translation.status !== 'pending_review' && (
+ {/* Approved state — read-only with copy buttons */}
+ {isApproved && (
+
+
+
+
+
{t('review.approved')}
+ {translation.approved_by_name && (
+
+ {t('review.reviewedBy')} {translation.approved_by_name}
+
+ )}
+
+
+ {translation.feedback && (
+
+
{t('review.feedback')}
+
{translation.feedback}
+
+ )}
+
+ )}
+
+ {/* Rejected state */}
+ {isRejected && (
+
+
+
+
+
{t('review.rejected')}
+ {translation.approved_by_name && (
+
+ {t('review.reviewedBy')} {translation.approved_by_name}
+
+ )}
+
+
+ {translation.feedback && (
+
+
{t('review.feedback')}
+
{translation.feedback}
+
+ )}
+
+ )}
+
+ {/* Other statuses (revision_requested, draft) */}
+ {!isPendingReview && !isApproved && !isRejected && (
-
+
{t('review.statusLabel')}: {translation.status.replace('_', ' ')}
{translation.approved_by_name && (
- {t('review.reviewedBy')}: {translation.approved_by_name}
+ {t('review.reviewedBy')} {translation.approved_by_name}
)}
diff --git a/client/src/pages/Translations.jsx b/client/src/pages/Translations.jsx
index 49c254a..ee30ab4 100644
--- a/client/src/pages/Translations.jsx
+++ b/client/src/pages/Translations.jsx
@@ -1,5 +1,5 @@
import { useState, useEffect, useContext, useMemo } from 'react'
-import { Plus, Search, LayoutGrid, List, ChevronUp, ChevronDown, Languages, Globe } from 'lucide-react'
+import { Plus, Search, LayoutGrid, List, ChevronUp, ChevronDown, Languages, Globe, FileEdit } from 'lucide-react'
import { AppContext } from '../App'
import { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../i18n/LanguageContext'
@@ -10,21 +10,8 @@ import { useToast } from '../components/ToastContainer'
import { SkeletonCard, SkeletonTable } from '../components/SkeletonLoader'
import TranslationDetailPanel from '../components/TranslationDetailPanel'
import ApproverMultiSelect from '../components/ApproverMultiSelect'
+import { AVAILABLE_LANGUAGES, TRANSLATION_STATUS_COLORS } from '../utils/translations'
-const STATUS_COLORS = {
- draft: 'bg-surface-tertiary text-text-secondary',
- pending_review: 'bg-amber-100 text-amber-700',
- approved: 'bg-emerald-100 text-emerald-700',
- rejected: 'bg-red-100 text-red-700',
- revision_requested: 'bg-orange-100 text-orange-700',
-}
-
-const AVAILABLE_LANGUAGES = [
- { code: 'AR', label: 'العربية' },
- { code: 'EN', label: 'English' },
- { code: 'FR', label: 'Français' },
- { code: 'ID', label: 'Bahasa Indonesia' },
-]
const SORT_OPTIONS = [
{ value: 'updated_at', dir: 'desc', labelKey: 'translations.sortRecentlyUpdated' },
@@ -45,8 +32,12 @@ export default function Translations() {
const [searchTerm, setSearchTerm] = useState('')
const [showCreateModal, setShowCreateModal] = useState(false)
const [selectedTranslation, setSelectedTranslation] = useState(null)
- const [newTranslation, setNewTranslation] = useState({ title: '', description: '', source_language: 'EN', source_content: '', brand_id: '', approver_ids: [] })
+ const [newTranslation, setNewTranslation] = useState({ title: '', source_language: 'EN', source_content: '', brand_id: '', post_id: '', approver_ids: [] })
const [saving, setSaving] = useState(false)
+ const [posts, setPosts] = useState([])
+ const [showCreatePost, setShowCreatePost] = useState(false)
+ const [newPostTitle, setNewPostTitle] = useState('')
+ const [creatingPost, setCreatingPost] = useState(false)
// Bulk select
const [selectedIds, setSelectedIds] = useState(new Set())
@@ -63,6 +54,7 @@ export default function Translations() {
useEffect(() => {
loadTranslations()
api.get('/users/assignable').then(res => setAssignableUsers(Array.isArray(res) ? res : [])).catch(() => {})
+ api.get('/posts').then(res => setPosts(Array.isArray(res) ? res : [])).catch(() => {})
}, [])
const loadTranslations = async () => {
@@ -91,10 +83,11 @@ export default function Translations() {
const created = await api.post('/translations', {
...newTranslation,
approver_ids: newTranslation.approver_ids.length > 0 ? newTranslation.approver_ids.join(',') : null,
+ post_id: newTranslation.post_id || null,
})
toast.success(t('translations.created'))
setShowCreateModal(false)
- setNewTranslation({ title: '', description: '', source_language: 'EN', source_content: '', brand_id: '', approver_ids: [] })
+ setNewTranslation({ title: '', source_language: 'EN', source_content: '', brand_id: '', post_id: '', approver_ids: [] })
loadTranslations()
setSelectedTranslation(created)
} catch (err) {
@@ -116,6 +109,24 @@ export default function Translations() {
}
}
+ const handleCreatePost = async () => {
+ if (!newPostTitle.trim()) return
+ setCreatingPost(true)
+ try {
+ const created = await api.post('/posts', { title: newPostTitle, status: 'draft' })
+ const postId = created.Id || created.id || created._id
+ setPosts(prev => [created, ...prev])
+ setNewTranslation(f => ({ ...f, post_id: String(postId) }))
+ setShowCreatePost(false)
+ setNewPostTitle('')
+ toast.success(t('translations.postCreated'))
+ } catch (err) {
+ toast.error(t('translations.postCreateFailed'))
+ } finally {
+ setCreatingPost(false)
+ }
+ }
+
const handleBulkDelete = async () => {
try {
await api.post('/translations/bulk-delete', { ids: [...selectedIds] })
@@ -314,7 +325,7 @@ export default function Translations() {
{tr.title}
-
+
{tr.status?.replace('_', ' ')}
@@ -323,11 +334,9 @@ export default function Translations() {
- {tr.description && (
-
{tr.description}
- )}
{tr.brand_name && {tr.brand_name}}
+ {tr.post_name && {tr.post_name}}
{tr.creator_name && by {tr.creator_name}}
{tr.translation_count || 0} {t('translations.languagesCount')}
@@ -384,7 +393,7 @@ export default function Translations() {
-
+
{tr.status?.replace('_', ' ')}
|
@@ -408,6 +417,7 @@ export default function Translations() {
onUpdate={loadTranslations}
onDelete={handleDelete}
assignableUsers={assignableUsers}
+ posts={posts}
/>
)}
@@ -454,6 +464,53 @@ export default function Translations() {
{brands.map(b =>
)}
+
+
+ {showCreatePost ? (
+
+ setNewPostTitle(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && handleCreatePost()}
+ className="flex-1 px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
+ placeholder={t('translations.newPostTitle')}
+ autoFocus
+ />
+
+
+
+ ) : (
+
+
+
+
+ )}
+
-
-
-