Files
dss/admin-ui/PHASE-2B-TRANSLATION-DICTIONARY-UI.md
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
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
2025-12-09 18:45:48 -03:00

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:

  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:

/**
 * 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

  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

// 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:

{
  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:

  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:

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:

// 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

  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

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