# 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.truncateText(ComponentHelpers.escapeHtml(dict.description), 80)}
` : ''}Please select a project from the header to manage translation dictionaries. Translation dictionaries map tokens between design systems.