Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
1647 lines
47 KiB
Markdown
1647 lines
47 KiB
Markdown
# 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 `
|
|
<div class="dictionary-card ${isSelected ? 'dictionary-card--selected' : ''}"
|
|
data-id="${dict.id}"
|
|
tabindex="0"
|
|
role="button"
|
|
aria-pressed="${isSelected}">
|
|
<div class="dictionary-card__header">
|
|
<h4 class="dictionary-card__title">${ComponentHelpers.escapeHtml(dict.name)}</h4>
|
|
${ComponentHelpers.createBadge(dict.status, statusColors[dict.status] || 'info')}
|
|
</div>
|
|
${dict.description ? `
|
|
<p class="dictionary-card__description">
|
|
${ComponentHelpers.truncateText(ComponentHelpers.escapeHtml(dict.description), 80)}
|
|
</p>
|
|
` : ''}
|
|
<div class="dictionary-card__meta">
|
|
<span class="dictionary-card__stat">
|
|
<span class="dictionary-card__stat-icon">🔗</span>
|
|
${mappingCount} mappings
|
|
</span>
|
|
<span class="dictionary-card__stat">
|
|
<span class="dictionary-card__stat-icon">📊</span>
|
|
${coverage}% coverage
|
|
</span>
|
|
</div>
|
|
<div class="dictionary-card__footer">
|
|
<span class="dictionary-card__date">
|
|
${ComponentHelpers.formatRelativeTime(dict.updatedAt)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<style>
|
|
.dictionary-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
background: var(--vscode-sidebar);
|
|
border-right: 1px solid var(--vscode-border);
|
|
}
|
|
|
|
.dictionary-list__header {
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--vscode-border);
|
|
}
|
|
|
|
.dictionary-list__title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.dictionary-list__filters {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.dictionary-list__search {
|
|
width: 100%;
|
|
padding: 6px 10px;
|
|
font-size: 12px;
|
|
border: 1px solid var(--vscode-input-border);
|
|
border-radius: 4px;
|
|
background: var(--vscode-input-background);
|
|
color: var(--vscode-input-foreground);
|
|
}
|
|
|
|
.dictionary-list__status-filter {
|
|
width: 100%;
|
|
padding: 6px 10px;
|
|
font-size: 12px;
|
|
border: 1px solid var(--vscode-input-border);
|
|
border-radius: 4px;
|
|
background: var(--vscode-input-background);
|
|
color: var(--vscode-input-foreground);
|
|
}
|
|
|
|
.dictionary-list__create-btn {
|
|
width: 100%;
|
|
padding: 8px 16px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
background: var(--vscode-button-background);
|
|
color: var(--vscode-button-foreground);
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.dictionary-list__create-btn:hover {
|
|
background: var(--vscode-button-hoverBackground);
|
|
}
|
|
|
|
.dictionary-list__content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 8px;
|
|
}
|
|
|
|
.dictionary-card {
|
|
padding: 12px;
|
|
margin-bottom: 8px;
|
|
background: var(--vscode-editor-background);
|
|
border: 1px solid var(--vscode-border);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.dictionary-card:hover {
|
|
border-color: var(--vscode-focusBorder);
|
|
}
|
|
|
|
.dictionary-card--selected {
|
|
border-color: var(--vscode-focusBorder);
|
|
background: var(--vscode-list-activeSelectionBackground);
|
|
}
|
|
|
|
.dictionary-card__header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.dictionary-card__title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
margin: 0;
|
|
flex: 1;
|
|
}
|
|
|
|
.dictionary-card__description {
|
|
font-size: 11px;
|
|
color: var(--vscode-descriptionForeground);
|
|
margin: 0 0 8px 0;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.dictionary-card__meta {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.dictionary-card__stat {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-size: 11px;
|
|
color: var(--vscode-descriptionForeground);
|
|
}
|
|
|
|
.dictionary-card__stat-icon {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.dictionary-card__footer {
|
|
font-size: 10px;
|
|
color: var(--vscode-descriptionForeground);
|
|
}
|
|
</style>
|
|
|
|
<div class="dictionary-list">
|
|
<div class="dictionary-list__header">
|
|
<h3 class="dictionary-list__title">Translation Dictionaries</h3>
|
|
<div class="dictionary-list__filters">
|
|
<input
|
|
type="text"
|
|
id="dictionary-search"
|
|
class="dictionary-list__search"
|
|
placeholder="Search dictionaries..."
|
|
/>
|
|
<select id="status-filter" class="dictionary-list__status-filter">
|
|
<option value="">All Status</option>
|
|
<option value="draft">Draft</option>
|
|
<option value="active">Active</option>
|
|
<option value="archived">Archived</option>
|
|
</select>
|
|
<button id="create-dictionary-btn" class="dictionary-list__create-btn">
|
|
+ Create Dictionary
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="dictionaries-container" class="dictionary-list__content">
|
|
${ComponentHelpers.renderLoading('Loading dictionaries...')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
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 = `
|
|
<style>
|
|
.translations-module {
|
|
display: flex;
|
|
height: 100%;
|
|
background: var(--vscode-editor-background);
|
|
}
|
|
|
|
.translations-module__sidebar {
|
|
width: 320px;
|
|
min-width: 280px;
|
|
max-width: 400px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.translations-module__main {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.translations-module__empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
padding: 48px;
|
|
text-align: center;
|
|
}
|
|
|
|
.translations-module__empty-icon {
|
|
font-size: 64px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.translations-module__empty-title {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.translations-module__empty-description {
|
|
font-size: 14px;
|
|
color: var(--vscode-descriptionForeground);
|
|
max-width: 400px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.no-project-warning {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
padding: 48px;
|
|
text-align: center;
|
|
}
|
|
</style>
|
|
|
|
${hasProject ? `
|
|
<div class="translations-module">
|
|
<div class="translations-module__sidebar">
|
|
<dictionary-list></dictionary-list>
|
|
</div>
|
|
<div class="translations-module__main">
|
|
<dictionary-detail></dictionary-detail>
|
|
</div>
|
|
</div>
|
|
` : `
|
|
<div class="no-project-warning">
|
|
<div style="font-size: 64px; margin-bottom: 24px;">📁</div>
|
|
<h2 style="font-size: 20px; font-weight: 600; margin-bottom: 12px;">No Project Selected</h2>
|
|
<p style="font-size: 14px; color: var(--vscode-descriptionForeground); max-width: 400px;">
|
|
Please select a project from the header to manage translation dictionaries.
|
|
Translation dictionaries map tokens between design systems.
|
|
</p>
|
|
</div>
|
|
`}
|
|
`;
|
|
}
|
|
}
|
|
|
|
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**
|