# Phase 2B: Translation Dictionary UI Implementation Plan **Version:** 1.0.0 **Target:** GPT-5.1-Codex-Max Implementation **Created:** December 2024 **Status:** Ready for Implementation --- ## Executive Summary This document provides a comprehensive implementation plan for the Translation Dictionary UI - a critical feature enabling token mapping between design systems (DSS Core Principle #2). The backend API is complete with 12 endpoints. This plan covers the frontend implementation using the existing Web Components architecture. --- ## 1. File Structure ### Files to CREATE ``` /home/overbits/dss/admin-ui/ ├── js/ │ ├── modules/ │ │ └── translations/ │ │ ├── TranslationsModule.js # MODIFY (replace placeholder) │ │ ├── components/ │ │ │ ├── DictionaryList.js # CREATE - List/filter dictionaries │ │ │ ├── DictionaryEditor.js # CREATE - Create/edit dictionary form │ │ │ ├── DictionaryDetail.js # CREATE - View dictionary with mappings │ │ │ ├── MappingTable.js # CREATE - Token mappings table │ │ │ ├── MappingEditor.js # CREATE - Single mapping editor modal │ │ │ ├── ValidationDashboard.js # CREATE - Validation results display │ │ │ ├── CoverageWidget.js # CREATE - Coverage visualization │ │ │ └── ImportExportPanel.js # CREATE - Bulk operations UI │ │ └── index.js # CREATE - Module exports │ ├── services/ │ │ └── translation-service.js # CREATE - API wrapper service │ └── stores/ │ └── translation-store.js # CREATE - Translation state management ``` ### Files to MODIFY ``` /home/overbits/dss/admin-ui/ ├── js/ │ ├── core/ │ │ └── router.js # Already has /translations route │ ├── stores/ │ │ └── context-store.js # Add translations context prompt │ └── modules/ │ └── translations/ │ └── TranslationsModule.js # Replace placeholder implementation ``` --- ## 2. Component Architecture ### 2.1 Component Hierarchy ``` TranslationsModule (Main Container) ├── DictionaryList (Left Panel - List View) │ ├── Search/Filter Bar │ ├── Dictionary Card (repeated) │ │ ├── Status Badge │ │ ├── Coverage Indicator │ │ └── Action Buttons │ └── Create Dictionary Button │ ├── DictionaryDetail (Center Panel - Selected Dictionary) │ ├── Header Section │ │ ├── Title/Description │ │ ├── Status Badge │ │ └── Action Bar (Edit, Validate, Export) │ ├── CoverageWidget │ ├── MappingTable │ │ ├── Column Headers │ │ ├── Mapping Rows (virtualized if large) │ │ └── Pagination │ └── ValidationDashboard (collapsible) │ ├── DictionaryEditor (Modal) │ ├── Form Fields │ │ ├── Name (required) │ │ ├── Description │ │ ├── Source System │ │ ├── Target System │ │ ├── Tags │ │ └── Status │ └── Save/Cancel Buttons │ ├── MappingEditor (Modal) │ ├── Source Token Input │ ├── Target Token Input │ ├── Transform Rule Builder │ ├── Confidence Score │ ├── Notes │ └── Validated Checkbox │ └── ImportExportPanel (Slide-over) ├── Import Tab │ ├── File Drop Zone │ ├── JSON Preview │ └── Import Button └── Export Tab ├── Format Selection └── Download Button ``` ### 2.2 Data Flow ``` ┌─────────────────────────────────────────────────────────────────┐ │ TranslationsModule │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ translation-store.js (State) ││ │ │ - dictionaries: [] ││ │ │ - selectedDictionaryId: string ││ │ │ - selectedDictionary: object (with mappings) ││ │ │ - validation: object ││ │ │ - coverage: object ││ │ │ - loading: { dictionaries, dictionary, validation, ... } ││ │ │ - errors: {} ││ │ │ - filters: { search, status, project } ││ │ └─────────────────────────────────────────────────────────────┘│ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐│ │ │ translation-service.js (API Layer) ││ │ │ - listDictionaries(filters) ││ │ │ - getDictionary(id) ││ │ │ - createDictionary(data) ││ │ │ - updateDictionary(id, data) ││ │ │ - deleteDictionary(id) ││ │ │ - createMapping(dictionaryId, data) ││ │ │ - updateMapping(dictionaryId, mappingId, data) ││ │ │ - deleteMapping(dictionaryId, mappingId) ││ │ │ - bulkImportMappings(dictionaryId, mappings) ││ │ │ - validateDictionary(id) ││ │ │ - getCoverage(id) ││ │ │ - exportDictionary(id) ││ │ └─────────────────────────────────────────────────────────────┘│ └─────────────────────────────────────────────────────────────────┘ ``` ### 2.3 State Management Strategy The module follows the existing patterns from `app-store.js`: 1. **Dedicated Store** (`translation-store.js`): - Extends EventTarget for reactive subscriptions - Request deduplication pattern (pendingRequests Map) - Loading/error state tracking per operation - Auto-sync with context-store for project filtering 2. **Component Subscriptions**: - Components subscribe to specific state keys - Cleanup unsubscribes in `disconnectedCallback()` - Use debouncing for search/filter inputs --- ## 3. Implementation Order ### Task 1: Create Translation Service (API Layer) **Priority:** P0 - Foundation **Effort:** 2 hours **Dependencies:** None Create `/js/services/translation-service.js`: ```javascript /** * translation-service.js * API wrapper for Translation Dictionary endpoints */ import apiClient from './api-client.js'; class TranslationService { constructor() { this.baseUrl = '/translations'; } // ========== Dictionary Operations ========== async listDictionaries(filters = {}) { const params = new URLSearchParams(); if (filters.projectId) params.set('projectId', filters.projectId); if (filters.status) params.set('status', filters.status); if (filters.limit) params.set('limit', filters.limit); if (filters.offset) params.set('offset', filters.offset); const queryString = params.toString(); const url = queryString ? `${this.baseUrl}?${queryString}` : this.baseUrl; return apiClient.request('GET', url); } async getDictionary(id) { return apiClient.request('GET', `${this.baseUrl}/${id}`); } async createDictionary(data) { return apiClient.request('POST', this.baseUrl, data); } async updateDictionary(id, data) { return apiClient.request('PUT', `${this.baseUrl}/${id}`, data); } async deleteDictionary(id) { return apiClient.request('DELETE', `${this.baseUrl}/${id}`); } // ========== Mapping Operations ========== async createMapping(dictionaryId, data) { return apiClient.request('POST', `${this.baseUrl}/${dictionaryId}/mappings`, data); } async updateMapping(dictionaryId, mappingId, data) { return apiClient.request('PUT', `${this.baseUrl}/${dictionaryId}/mappings/${mappingId}`, data); } async deleteMapping(dictionaryId, mappingId) { return apiClient.request('DELETE', `${this.baseUrl}/${dictionaryId}/mappings/${mappingId}`); } async bulkImportMappings(dictionaryId, mappings) { return apiClient.request('POST', `${this.baseUrl}/${dictionaryId}/mappings/bulk`, { mappings }); } // ========== Validation & Analysis ========== async validateDictionary(id) { return apiClient.request('GET', `${this.baseUrl}/${id}/validate`); } async getCoverage(id) { return apiClient.request('GET', `${this.baseUrl}/${id}/coverage`); } async exportDictionary(id) { return apiClient.request('GET', `${this.baseUrl}/${id}/export`); } } export default new TranslationService(); ``` --- ### Task 2: Create Translation Store (State Management) **Priority:** P0 - Foundation **Effort:** 3 hours **Dependencies:** Task 1 Create `/js/stores/translation-store.js`: ```javascript /** * translation-store.js * Centralized state management for Translation Dictionaries */ import translationService from '../services/translation-service.js'; import contextStore from './context-store.js'; class TranslationStore extends EventTarget { constructor() { super(); this.state = { // Data dictionaries: [], selectedDictionaryId: null, selectedDictionary: null, validation: null, coverage: null, // UI State filters: { search: '', status: null, projectId: null }, pagination: { total: 0, limit: 50, offset: 0 }, // Loading states loading: { dictionaries: false, dictionary: false, validation: false, coverage: false, creating: false, updating: false, deleting: false, importing: false }, // Errors errors: {} }; this.pendingRequests = new Map(); // Subscribe to project changes contextStore.subscribeToKey('projectId', (newProjectId) => { if (newProjectId) { this.setFilter('projectId', newProjectId); this.fetchDictionaries(); } }); } // === State Access === get(key) { return key ? this.state[key] : this.state; } getState() { return { ...this.state }; } // === State Updates === set(updates) { const prevState = { ...this.state }; this.state = { ...this.state, ...updates }; this._notify(updates, prevState); } setLoading(key, loading = true) { this.set({ loading: { ...this.state.loading, [key]: loading } }); } setError(key, error) { this.set({ errors: { ...this.state.errors, [key]: error } }); } clearError(key) { const errors = { ...this.state.errors }; delete errors[key]; this.set({ errors }); } setFilter(key, value) { this.set({ filters: { ...this.state.filters, [key]: value } }); } // === Subscriptions === subscribe(callback) { const handler = (event) => callback(event.detail); this.addEventListener('state-change', handler); return () => this.removeEventListener('state-change', handler); } subscribeToKey(key, callback) { const handler = (event) => { const { changes } = event.detail; if (changes[key]) { callback(changes[key].newValue, changes[key].oldValue); } }; this.addEventListener('state-change', handler); return () => this.removeEventListener('state-change', handler); } _notify(updates, prevState) { const changes = {}; for (const key in updates) { if (JSON.stringify(prevState[key]) !== JSON.stringify(updates[key])) { changes[key] = { oldValue: prevState[key], newValue: updates[key] }; } } if (Object.keys(changes).length > 0) { this.dispatchEvent(new CustomEvent('state-change', { detail: { state: this.state, changes } })); } } // === Dictionary Operations === async fetchDictionaries() { const requestKey = 'dictionaries'; if (this.pendingRequests.has(requestKey)) { return this.pendingRequests.get(requestKey); } const requestPromise = (async () => { this.setLoading('dictionaries', true); this.clearError('dictionaries'); try { const { filters, pagination } = this.state; const result = await translationService.listDictionaries({ projectId: filters.projectId, status: filters.status, limit: pagination.limit, offset: pagination.offset }); this.set({ dictionaries: result.dictionaries || [], pagination: { ...this.state.pagination, total: result.total || 0 } }); return result.dictionaries; } catch (error) { this.setError('dictionaries', error.message); throw error; } finally { this.setLoading('dictionaries', false); this.pendingRequests.delete(requestKey); } })(); this.pendingRequests.set(requestKey, requestPromise); return requestPromise; } async selectDictionary(id) { if (!id) { this.set({ selectedDictionaryId: null, selectedDictionary: null, validation: null, coverage: null }); return; } this.set({ selectedDictionaryId: id }); this.setLoading('dictionary', true); this.clearError('dictionary'); try { const result = await translationService.getDictionary(id); this.set({ selectedDictionary: result.dictionary }); // Auto-fetch coverage for selected dictionary this.fetchCoverage(id); return result.dictionary; } catch (error) { this.setError('dictionary', error.message); throw error; } finally { this.setLoading('dictionary', false); } } async createDictionary(data) { this.setLoading('creating', true); this.clearError('creating'); try { const result = await translationService.createDictionary({ ...data, projectId: this.state.filters.projectId || contextStore.get('projectId') }); // Refresh list and select new dictionary await this.fetchDictionaries(); await this.selectDictionary(result.dictionary.id); return result.dictionary; } catch (error) { this.setError('creating', error.message); throw error; } finally { this.setLoading('creating', false); } } async updateDictionary(id, data) { this.setLoading('updating', true); this.clearError('updating'); try { const result = await translationService.updateDictionary(id, data); // Update in list this.set({ dictionaries: this.state.dictionaries.map(d => d.id === id ? result.dictionary : d ), selectedDictionary: this.state.selectedDictionaryId === id ? result.dictionary : this.state.selectedDictionary }); return result.dictionary; } catch (error) { this.setError('updating', error.message); throw error; } finally { this.setLoading('updating', false); } } async deleteDictionary(id) { this.setLoading('deleting', true); this.clearError('deleting'); try { await translationService.deleteDictionary(id); // Remove from list this.set({ dictionaries: this.state.dictionaries.filter(d => d.id !== id), selectedDictionaryId: this.state.selectedDictionaryId === id ? null : this.state.selectedDictionaryId, selectedDictionary: this.state.selectedDictionaryId === id ? null : this.state.selectedDictionary }); } catch (error) { this.setError('deleting', error.message); throw error; } finally { this.setLoading('deleting', false); } } // === Mapping Operations === async createMapping(data) { const dictionaryId = this.state.selectedDictionaryId; if (!dictionaryId) throw new Error('No dictionary selected'); this.setLoading('creating', true); this.clearError('creating'); try { const result = await translationService.createMapping(dictionaryId, data); // Refresh selected dictionary to get updated mappings await this.selectDictionary(dictionaryId); return result.mapping; } catch (error) { this.setError('creating', error.message); throw error; } finally { this.setLoading('creating', false); } } async updateMapping(mappingId, data) { const dictionaryId = this.state.selectedDictionaryId; if (!dictionaryId) throw new Error('No dictionary selected'); this.setLoading('updating', true); this.clearError('updating'); try { const result = await translationService.updateMapping(dictionaryId, mappingId, data); // Update mapping in place if (this.state.selectedDictionary) { const mappings = this.state.selectedDictionary.Mappings || []; this.set({ selectedDictionary: { ...this.state.selectedDictionary, Mappings: mappings.map(m => m.id === mappingId ? result.mapping : m) } }); } return result.mapping; } catch (error) { this.setError('updating', error.message); throw error; } finally { this.setLoading('updating', false); } } async deleteMapping(mappingId) { const dictionaryId = this.state.selectedDictionaryId; if (!dictionaryId) throw new Error('No dictionary selected'); this.setLoading('deleting', true); this.clearError('deleting'); try { await translationService.deleteMapping(dictionaryId, mappingId); // Remove mapping from local state if (this.state.selectedDictionary) { const mappings = this.state.selectedDictionary.Mappings || []; this.set({ selectedDictionary: { ...this.state.selectedDictionary, Mappings: mappings.filter(m => m.id !== mappingId) } }); } } catch (error) { this.setError('deleting', error.message); throw error; } finally { this.setLoading('deleting', false); } } async bulkImportMappings(mappings) { const dictionaryId = this.state.selectedDictionaryId; if (!dictionaryId) throw new Error('No dictionary selected'); this.setLoading('importing', true); this.clearError('importing'); try { const result = await translationService.bulkImportMappings(dictionaryId, mappings); // Refresh selected dictionary await this.selectDictionary(dictionaryId); return result; } catch (error) { this.setError('importing', error.message); throw error; } finally { this.setLoading('importing', false); } } // === Validation & Analysis === async fetchValidation(id = null) { const dictionaryId = id || this.state.selectedDictionaryId; if (!dictionaryId) return; this.setLoading('validation', true); this.clearError('validation'); try { const result = await translationService.validateDictionary(dictionaryId); this.set({ validation: result.validation }); return result.validation; } catch (error) { this.setError('validation', error.message); throw error; } finally { this.setLoading('validation', false); } } async fetchCoverage(id = null) { const dictionaryId = id || this.state.selectedDictionaryId; if (!dictionaryId) return; this.setLoading('coverage', true); this.clearError('coverage'); try { const result = await translationService.getCoverage(dictionaryId); this.set({ coverage: result.coverage }); return result.coverage; } catch (error) { this.setError('coverage', error.message); throw error; } finally { this.setLoading('coverage', false); } } async exportDictionary(id = null) { const dictionaryId = id || this.state.selectedDictionaryId; if (!dictionaryId) throw new Error('No dictionary selected'); try { const result = await translationService.exportDictionary(dictionaryId); return result.export; } catch (error) { this.setError('export', error.message); throw error; } } } export default new TranslationStore(); ``` --- ### Task 3: Create Dictionary List Component **Priority:** P1 - Core UI **Effort:** 4 hours **Dependencies:** Tasks 1, 2 Create `/js/modules/translations/components/DictionaryList.js`: ```javascript /** * DictionaryList.js * List and filter translation dictionaries */ import { ComponentHelpers } from '../../../utils/component-helpers.js'; import translationStore from '../../../stores/translation-store.js'; class DictionaryList extends HTMLElement { constructor() { super(); this.unsubscribe = null; this.searchDebounceTimer = null; } connectedCallback() { this.render(); this.setupEventListeners(); this.subscribeToStore(); } disconnectedCallback() { if (this.unsubscribe) { this.unsubscribe(); } if (this.searchDebounceTimer) { clearTimeout(this.searchDebounceTimer); } } subscribeToStore() { this.unsubscribe = translationStore.subscribe(({ state, changes }) => { if (changes.dictionaries || changes.loading || changes.selectedDictionaryId) { this.renderList(); } }); } setupEventListeners() { // Search input const searchInput = this.querySelector('#dictionary-search'); if (searchInput) { searchInput.addEventListener('input', (e) => this.handleSearch(e.target.value)); } // Status filter const statusFilter = this.querySelector('#status-filter'); if (statusFilter) { statusFilter.addEventListener('change', (e) => { translationStore.setFilter('status', e.target.value || null); translationStore.fetchDictionaries(); }); } // Create button const createBtn = this.querySelector('#create-dictionary-btn'); if (createBtn) { createBtn.addEventListener('click', () => { this.dispatchEvent(new CustomEvent('create-dictionary', { bubbles: true, composed: true })); }); } } handleSearch(value) { if (this.searchDebounceTimer) { clearTimeout(this.searchDebounceTimer); } this.searchDebounceTimer = setTimeout(() => { translationStore.setFilter('search', value); // Note: Search is client-side filter for now this.renderList(); }, 300); } handleSelectDictionary(id) { translationStore.selectDictionary(id); this.dispatchEvent(new CustomEvent('dictionary-selected', { detail: { id }, bubbles: true, composed: true })); } renderList() { const container = this.querySelector('#dictionaries-container'); if (!container) return; const state = translationStore.getState(); const { dictionaries, loading, filters, selectedDictionaryId } = state; if (loading.dictionaries) { container.innerHTML = ComponentHelpers.renderLoading('Loading dictionaries...'); return; } if (state.errors.dictionaries) { container.innerHTML = ComponentHelpers.renderError('Failed to load dictionaries', { message: state.errors.dictionaries }); return; } // Apply client-side search filter let filtered = dictionaries; if (filters.search) { const search = filters.search.toLowerCase(); filtered = dictionaries.filter(d => d.name.toLowerCase().includes(search) || (d.description && d.description.toLowerCase().includes(search)) ); } if (filtered.length === 0) { container.innerHTML = ComponentHelpers.renderEmpty( filters.search ? 'No dictionaries match your search' : 'No translation dictionaries yet', filters.search ? '🔍' : '📚' ); return; } container.innerHTML = filtered.map(dict => this.renderDictionaryCard(dict, selectedDictionaryId === dict.id)).join(''); // Add click handlers container.querySelectorAll('.dictionary-card').forEach(card => { card.addEventListener('click', () => { this.handleSelectDictionary(card.dataset.id); }); }); } renderDictionaryCard(dict, isSelected) { const statusColors = { draft: 'warning', active: 'success', archived: 'error' }; const coverage = dict.metadata?.coverage || 0; const mappingCount = dict.mappingCount || 0; return `

${ComponentHelpers.escapeHtml(dict.name)}

${ComponentHelpers.createBadge(dict.status, statusColors[dict.status] || 'info')}
${dict.description ? `

${ComponentHelpers.truncateText(ComponentHelpers.escapeHtml(dict.description), 80)}

` : ''}
🔗 ${mappingCount} mappings 📊 ${coverage}% coverage
`; } render() { this.innerHTML = `

Translation Dictionaries

${ComponentHelpers.renderLoading('Loading dictionaries...')}
`; } } customElements.define('dictionary-list', DictionaryList); export default DictionaryList; ``` --- ### Task 4: Create Dictionary Detail & Mapping Table Components **Priority:** P1 - Core UI **Effort:** 6 hours **Dependencies:** Tasks 1-3 Create `/js/modules/translations/components/DictionaryDetail.js` and `/js/modules/translations/components/MappingTable.js`. **DictionaryDetail.js** - Main detail view with header, action bar, and mapping table. **MappingTable.js** - Table component for displaying and editing token mappings: - Sortable columns (sourceToken, targetToken, validated, confidence) - Inline validation toggle - Edit/Delete actions per row - Pagination for large datasets --- ### Task 5: Create Editor Modals (Dictionary & Mapping) **Priority:** P1 - Core UI **Effort:** 5 hours **Dependencies:** Tasks 1-4 Create: - `/js/modules/translations/components/DictionaryEditor.js` - Form for create/edit dictionary - `/js/modules/translations/components/MappingEditor.js` - Modal for create/edit mapping Key patterns: - Form validation before submit - Loading states on submit button - Error display within modal - Close on successful save - ESC key to close --- ### Task 6: Create Validation Dashboard & Coverage Widget **Priority:** P2 - Analysis Features **Effort:** 4 hours **Dependencies:** Tasks 1-4 Create: - `/js/modules/translations/components/ValidationDashboard.js` - `/js/modules/translations/components/CoverageWidget.js` **CoverageWidget** - Visual percentage display: - Circular progress indicator - Color coding (green >80%, yellow >50%, red <50%) - Total/Mapped/Unmapped token counts **ValidationDashboard** - Expandable panel showing: - Overall validation status - Error list with line references - Warning list - Suggested fixes --- ### Task 7: Create Import/Export Panel **Priority:** P2 - Bulk Operations **Effort:** 4 hours **Dependencies:** Tasks 1-5 Create `/js/modules/translations/components/ImportExportPanel.js`: - Import: File drop zone, JSON validation, preview, conflict resolution - Export: Format selection (JSON), download trigger - Results summary (created/updated/errors) --- ### Task 8: Integrate TranslationsModule & Final Testing **Priority:** P0 - Integration **Effort:** 4 hours **Dependencies:** All previous tasks Update `/js/modules/translations/TranslationsModule.js`: ```javascript /** * TranslationsModule.js * Main container for Translation Dictionary management (DSS Principle #2) */ import translationStore from '../../stores/translation-store.js'; import contextStore from '../../stores/context-store.js'; import { ComponentHelpers } from '../../utils/component-helpers.js'; // Import components import './components/DictionaryList.js'; import './components/DictionaryDetail.js'; import './components/DictionaryEditor.js'; import './components/MappingEditor.js'; import './components/ValidationDashboard.js'; import './components/CoverageWidget.js'; import './components/ImportExportPanel.js'; class TranslationsModule extends HTMLElement { constructor() { super(); this.unsubscribe = []; this.modals = { dictionaryEditor: null, mappingEditor: null, importExport: null }; } connectedCallback() { this.render(); this.setupEventListeners(); this.loadInitialData(); } disconnectedCallback() { this.unsubscribe.forEach(fn => fn()); this.unsubscribe = []; } async loadInitialData() { // Get current project from context const projectId = contextStore.get('projectId'); if (projectId) { translationStore.setFilter('projectId', projectId); await translationStore.fetchDictionaries(); } } setupEventListeners() { // Listen for create dictionary request this.addEventListener('create-dictionary', () => this.openDictionaryEditor()); // Listen for edit dictionary request this.addEventListener('edit-dictionary', (e) => this.openDictionaryEditor(e.detail.dictionary)); // Listen for dictionary selection this.addEventListener('dictionary-selected', (e) => { const detailPanel = this.querySelector('dictionary-detail'); if (detailPanel) { detailPanel.setAttribute('dictionary-id', e.detail.id); } }); // Listen for create mapping request this.addEventListener('create-mapping', () => this.openMappingEditor()); // Listen for edit mapping request this.addEventListener('edit-mapping', (e) => this.openMappingEditor(e.detail.mapping)); // Listen for import/export request this.addEventListener('open-import-export', () => this.openImportExport()); // Subscribe to project changes this.unsubscribe.push( contextStore.subscribeToKey('projectId', (projectId) => { if (projectId) { translationStore.setFilter('projectId', projectId); translationStore.fetchDictionaries(); } }) ); } openDictionaryEditor(dictionary = null) { // Implementation: Open modal or slide-over for dictionary creation/editing const editor = document.createElement('dictionary-editor'); if (dictionary) { editor.setAttribute('dictionary-id', dictionary.id); } this.appendChild(editor); } openMappingEditor(mapping = null) { const editor = document.createElement('mapping-editor'); if (mapping) { editor.setAttribute('mapping-id', mapping.id); } this.appendChild(editor); } openImportExport() { const panel = document.createElement('import-export-panel'); this.appendChild(panel); } render() { const hasProject = contextStore.hasProject(); this.innerHTML = ` ${hasProject ? `
` : `
📁

No Project Selected

Please select a project from the header to manage translation dictionaries. Translation dictionaries map tokens between design systems.

`} `; } } customElements.define('dss-translations-module', TranslationsModule); export default TranslationsModule; ``` --- ## 4. UI/UX Design Patterns ### 4.1 Layout Approach **Master-Detail Pattern** (similar to VS Code): - Left sidebar: Dictionary list (320px fixed) - Center: Selected dictionary detail (flexible) - Right: Contextual panels (validation, import/export) as slide-overs ### 4.2 User Workflows **Workflow 1: Create New Dictionary** 1. User clicks "+ Create Dictionary" button 2. Modal opens with form 3. Fill: Name (required), Description, Source/Target System, Tags 4. Click "Create" -> Dictionary created and selected 5. Empty mapping table shown with "+ Add Mapping" CTA **Workflow 2: Add Token Mappings** 1. Select dictionary from list 2. Click "+ Add Mapping" in mapping table 3. Modal opens: Source Token, Target Token, Transform Rule (optional) 4. Click "Save" -> Mapping added to table 5. Coverage widget updates automatically **Workflow 3: Bulk Import Mappings** 1. Select dictionary 2. Click "Import" button in action bar 3. Slide-over opens with file drop zone 4. Drop/select JSON file 5. Preview shows parsed mappings 6. Click "Import" -> Progress shown 7. Results summary: X created, Y updated, Z errors **Workflow 4: Validate Dictionary** 1. Select dictionary 2. Click "Validate" button 3. Loading indicator in validation dashboard 4. Results shown: errors, warnings, suggestions 5. Click error to scroll to mapping ### 4.3 Error Handling ```javascript // Standard error display pattern const handleApiError = async (operation, callback) => { try { await callback(); ComponentHelpers.showToast?.(`${operation} successful`, 'success'); } catch (error) { ComponentHelpers.showToast?.(`${operation} failed: ${error.message}`, 'error'); // Errors are also stored in translationStore.state.errors } }; ``` ### 4.4 Loading States Each operation has dedicated loading state: - `loading.dictionaries` - List loading - `loading.dictionary` - Detail loading - `loading.validation` - Validation running - `loading.creating` - Create operation - `loading.updating` - Update operation - `loading.deleting` - Delete operation - `loading.importing` - Bulk import Components check these states and render appropriate UI: - Skeleton loaders for lists - Spinner overlay for actions - Disabled buttons during operations --- ## 5. Integration Points ### 5.1 Navigation Access The route `/translations` is already registered in `/js/core/router.js`: ```javascript { path: '/translations', name: 'Translations', handler: () => this.loadModule('dss-translations-module', () => import('../modules/translations/TranslationsModule.js')) } ``` Access via: - Direct URL: `#translations` - Navigation item in sidebar (if configured) - Router navigation: `router.navigate('translations')` ### 5.2 Context Store Integration Add translations context prompt in `/js/stores/context-store.js`: ```javascript const PAGE_CONTEXT_PROMPTS = { // ... existing prompts ... translations: 'You are helping manage translation dictionaries that map tokens between design systems. The user can create dictionaries, add token mappings, validate coverage, and import/export mappings.' }; ``` ### 5.3 Project Context Sync Translation dictionaries are project-scoped. The module: 1. Reads `projectId` from `contextStore` on mount 2. Subscribes to `projectId` changes 3. Filters dictionaries by current project 4. Passes `projectId` when creating new dictionaries ### 5.4 Toast Notifications Use existing notification system: ```javascript import { ComponentHelpers } from '../utils/component-helpers.js'; // Success ComponentHelpers.showToast?.('Dictionary created successfully', 'success'); // Error ComponentHelpers.showToast?.('Failed to create dictionary', 'error'); // Info ComponentHelpers.showToast?.('Import in progress...', 'info'); ``` --- ## 6. Technical Decisions ### 6.1 Reusable Components **Components to create that may be reused:** 1. **Modal** - Generic modal wrapper (if not existing) 2. **ConfirmDialog** - Confirmation before destructive actions 3. **FileDropZone** - Drag-and-drop file upload area 4. **ProgressRing** - Circular progress indicator for coverage 5. **DataTable** - Sortable, paginated table (extend for mappings) ### 6.2 API Calling Patterns Follow existing patterns from `app-store.js`: ```javascript // Request deduplication async fetchDictionaries() { const requestKey = 'dictionaries'; // Return existing promise if in flight if (this.pendingRequests.has(requestKey)) { return this.pendingRequests.get(requestKey); } const requestPromise = (async () => { try { // ... API call } finally { this.pendingRequests.delete(requestKey); } })(); this.pendingRequests.set(requestKey, requestPromise); return requestPromise; } ``` ### 6.3 Form Validation Client-side validation before API calls: ```javascript const validateDictionaryForm = (data) => { const errors = {}; if (!data.name || data.name.trim().length < 3) { errors.name = 'Name must be at least 3 characters'; } if (data.name && data.name.length > 255) { errors.name = 'Name must be less than 255 characters'; } return { isValid: Object.keys(errors).length === 0, errors }; }; ``` ### 6.4 Keyboard Navigation Implement for accessibility: - Tab through list items - Enter to select - Arrow keys in list - ESC to close modals - Focus trap in modals --- ## 7. Testing Checklist ### Unit Tests - [ ] TranslationService API methods - [ ] TranslationStore state management - [ ] Form validation functions - [ ] ComponentHelpers utilities ### Integration Tests - [ ] Create dictionary flow - [ ] Add/edit/delete mapping flow - [ ] Bulk import flow - [ ] Validation flow - [ ] Export flow ### E2E Tests - [ ] Full workflow: Create dictionary -> Add mappings -> Validate -> Export - [ ] Error handling: Invalid data, network errors - [ ] Responsive behavior --- ## 8. Success Criteria 1. **Functional Requirements:** - [ ] Can list, filter, and search dictionaries - [ ] Can create/edit/delete dictionaries - [ ] Can add/edit/delete token mappings - [ ] Can bulk import mappings from JSON - [ ] Can validate dictionary mappings - [ ] Can view coverage statistics - [ ] Can export dictionary as JSON 2. **Non-Functional Requirements:** - [ ] Loads in < 2s on initial page visit - [ ] Responsive updates after actions (< 500ms) - [ ] Proper error handling and user feedback - [ ] Accessible (keyboard navigation, ARIA labels) - [ ] Works with existing project context 3. **Code Quality:** - [ ] Follows existing codebase patterns - [ ] No console errors in production - [ ] Clean separation of concerns (service/store/components) --- ## Appendix A: Backend API Reference | Endpoint | Method | Description | |----------|--------|-------------| | `/api/translations` | GET | List dictionaries (pagination, filters) | | `/api/translations/:id` | GET | Get dictionary with mappings | | `/api/translations` | POST | Create dictionary | | `/api/translations/:id` | PUT | Update dictionary | | `/api/translations/:id` | DELETE | Archive dictionary | | `/api/translations/:id/mappings` | POST | Create mapping | | `/api/translations/:id/mappings/:mappingId` | PUT | Update mapping | | `/api/translations/:id/mappings/:mappingId` | DELETE | Delete mapping | | `/api/translations/:id/mappings/bulk` | POST | Bulk import mappings | | `/api/translations/:id/validate` | GET | Run validation | | `/api/translations/:id/coverage` | GET | Calculate coverage | | `/api/translations/:id/export` | GET | Export dictionary | --- ## Appendix B: Data Models ### TranslationDictionary ```typescript interface TranslationDictionary { id: string; // UUID name: string; // 3-255 chars description: string | null; projectId: string; // UUID createdBy: string; // UUID status: 'draft' | 'active' | 'archived'; version: number; metadata: { sourceSystem: string | null; targetSystem: string | null; coverage: number; validationStatus: 'pending' | 'valid' | 'invalid'; lastValidated: string | null; tags: string[]; }; createdAt: string; updatedAt: string; Mappings?: TranslationMapping[]; } ``` ### TranslationMapping ```typescript interface TranslationMapping { id: string; // UUID dictionaryId: string; // UUID sourceToken: string; targetToken: string; transformRule: object | null; validated: boolean; confidence: number; // 0-1 notes: string | null; createdAt: string; updatedAt: string; } ``` --- **End of Implementation Plan**