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
47 KiB
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:
-
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
-
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:
/**
* 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:
/**
* 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:
/**
* 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:
/**
* 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
- User clicks "+ Create Dictionary" button
- Modal opens with form
- Fill: Name (required), Description, Source/Target System, Tags
- Click "Create" -> Dictionary created and selected
- Empty mapping table shown with "+ Add Mapping" CTA
Workflow 2: Add Token Mappings
- Select dictionary from list
- Click "+ Add Mapping" in mapping table
- Modal opens: Source Token, Target Token, Transform Rule (optional)
- Click "Save" -> Mapping added to table
- Coverage widget updates automatically
Workflow 3: Bulk Import Mappings
- Select dictionary
- Click "Import" button in action bar
- Slide-over opens with file drop zone
- Drop/select JSON file
- Preview shows parsed mappings
- Click "Import" -> Progress shown
- Results summary: X created, Y updated, Z errors
Workflow 4: Validate Dictionary
- Select dictionary
- Click "Validate" button
- Loading indicator in validation dashboard
- Results shown: errors, warnings, suggestions
- Click error to scroll to mapping
4.3 Error Handling
// 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 loadingloading.dictionary- Detail loadingloading.validation- Validation runningloading.creating- Create operationloading.updating- Update operationloading.deleting- Delete operationloading.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:
{
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:
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:
- Reads
projectIdfromcontextStoreon mount - Subscribes to
projectIdchanges - Filters dictionaries by current project
- Passes
projectIdwhen creating new dictionaries
5.4 Toast Notifications
Use existing notification system:
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:
- Modal - Generic modal wrapper (if not existing)
- ConfirmDialog - Confirmation before destructive actions
- FileDropZone - Drag-and-drop file upload area
- ProgressRing - Circular progress indicator for coverage
- DataTable - Sortable, paginated table (extend for mappings)
6.2 API Calling Patterns
Follow existing patterns from app-store.js:
// 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:
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
-
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
-
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
-
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
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
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