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
3196 lines
98 KiB
Markdown
3196 lines
98 KiB
Markdown
# Translation Dictionary System - Implementation Plan
|
|
|
|
**Version:** 1.0.0
|
|
**Date:** December 9, 2024
|
|
**Status:** PLANNING
|
|
**Author:** Architecture Planning (Gemini 3 Pro Simulation)
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
This document provides a comprehensive implementation plan for the **Translation Dictionary System** - a critical missing component in the DSS Python core that enables mapping between external design token formats and the canonical DSS structure.
|
|
|
|
### Why This Matters
|
|
|
|
The Translation Dictionary System is the **keystone** of the entire DSS philosophy:
|
|
- DSS Core is immutable - external systems adapt to DSS, not vice versa
|
|
- Each client project needs its own mappings from legacy tokens to DSS canonical tokens
|
|
- Custom props must be isolated in client-specific namespaces
|
|
- Full traceability from source to DSS to output
|
|
|
|
### Current State
|
|
|
|
| Component | Status | Location |
|
|
|-----------|--------|----------|
|
|
| DSS Core Principles | Documented | `DSS_PRINCIPLES.md` |
|
|
| Translation Schema | Documented | `DSS_PRINCIPLES.md` |
|
|
| Python Implementation | **MISSING** | Should be `dss/translations/` |
|
|
| MCP Integration | **BLOCKED** | Depends on Python implementation |
|
|
|
|
### Implementation Impact
|
|
|
|
| Phase | Current State | After Implementation |
|
|
|-------|---------------|----------------------|
|
|
| Workflow 1 (Figma Import) | Working | Enhanced with translation |
|
|
| Workflow 2 (Skin Loading) | 60% | 100% - Full skin support |
|
|
| Workflow 3 (Design Apply) | 10% | 100% - Full token resolution |
|
|
|
|
---
|
|
|
|
## 1. Architecture Overview
|
|
|
|
### 1.1 System Architecture Diagram
|
|
|
|
```
|
|
DSS TRANSLATION DICTIONARY SYSTEM
|
|
+=======================================================================================+
|
|
| |
|
|
| EXTERNAL SOURCES TRANSLATION LAYER DSS CORE |
|
|
| ================ ================= ======== |
|
|
| |
|
|
| +-------------+ +------------------+ +-----------+ |
|
|
| | Figma |----+ | Translation | | Canonical | |
|
|
| | Tokens | | | Dictionary | | Tokens | |
|
|
| +-------------+ | | Loader | | | |
|
|
| | +--------+---------+ | color. | |
|
|
| +-------------+ | Load & | | primary. | |
|
|
| | Legacy CSS |----+----Parse----> +--------v---------+ Resolve | 500 | |
|
|
| | Variables | | | Translation |----------> | | |
|
|
| +-------------+ | | Registry | | spacing. | |
|
|
| | +--------+---------+ | md | |
|
|
| +-------------+ | | | | |
|
|
| | HeroUI/ |----+ +--------v---------+ | etc. | |
|
|
| | shadcn | | | Token | +-----------+ |
|
|
| +-------------+ | | Resolver | | |
|
|
| | +--------+---------+ | |
|
|
| +-------------+ | | | |
|
|
| | Custom |----+ +--------v---------+ +-----v-----+ |
|
|
| | JSON/YAML | | Custom Props | | Merged | |
|
|
| +-------------+ | Merger | | Theme | |
|
|
| +--------+---------+ | Output | |
|
|
| | +-----------+ |
|
|
| +--------v---------+ | |
|
|
| | Validation | | |
|
|
| | Engine | v |
|
|
| +------------------+ OUTPUT FILES |
|
|
| - CSS vars |
|
|
| - SCSS vars |
|
|
| - JSON tokens |
|
|
| - TypeScript |
|
|
| |
|
|
+=======================================================================================+
|
|
```
|
|
|
|
### 1.2 Data Flow Diagram
|
|
|
|
```
|
|
TRANSLATION DICTIONARY DATA FLOW
|
|
+--------------------------------------------------------------------------------+
|
|
| |
|
|
| PROJECT |
|
|
| +----------------------------+ |
|
|
| | .dss/ | |
|
|
| | config.json |--------> Project Configuration |
|
|
| | translations/ | |
|
|
| | figma.json -|--------> Figma Token Mappings |
|
|
| | legacy-css.json -|--------> Legacy CSS Mappings |
|
|
| | heroui.json -|--------> HeroUI Mappings |
|
|
| | shadcn.json -|--------> shadcn Mappings |
|
|
| | custom.json -|--------> Custom Props Extensions |
|
|
| +----------------------------+ |
|
|
| | |
|
|
| v |
|
|
| +----------------------------+ |
|
|
| | TranslationDictionaryLoader| <-- Single entry point |
|
|
| +----------------------------+ |
|
|
| | |
|
|
| v |
|
|
| +----------------------------+ +------------------------+ |
|
|
| | TranslationRegistry |<--->| ValidationEngine | |
|
|
| | (in-memory cache) | | - Schema validation | |
|
|
| +----------------------------+ | - DSS canonical check | |
|
|
| | | - Conflict detection | |
|
|
| | +------------------------+ |
|
|
| v |
|
|
| +----------------------------+ |
|
|
| | TokenResolver | |
|
|
| | - Resolve source -> DSS | |
|
|
| | - Resolve DSS -> source | <-- BIDIRECTIONAL |
|
|
| | - Handle aliases | |
|
|
| | - Chain references | |
|
|
| +----------------------------+ |
|
|
| | |
|
|
| v |
|
|
| +----------------------------+ +------------------------+ |
|
|
| | ThemeMerger |<--->| Base Theme | |
|
|
| | - Merge base + custom | | (light/dark) | |
|
|
| | - Apply translations | +------------------------+ |
|
|
| | - Generate resolved theme | |
|
|
| +----------------------------+ |
|
|
| | |
|
|
| v |
|
|
| +----------------------------+ |
|
|
| | ResolvedProjectTheme | <-- Final output |
|
|
| | - All tokens resolved | |
|
|
| | - Custom props merged | |
|
|
| | - Ready for export | |
|
|
| +----------------------------+ |
|
|
| |
|
|
+--------------------------------------------------------------------------------+
|
|
```
|
|
|
|
### 1.3 Module Integration Diagram
|
|
|
|
```
|
|
DSS MODULE INTEGRATION
|
|
+-------------------------------------------------------------+
|
|
| |
|
|
| dss/__init__.py |
|
|
| +-----------------------------------------------------------+
|
|
| | |
|
|
| | EXISTING MODULES NEW MODULE |
|
|
| | ================= ========== |
|
|
| | |
|
|
| | +-----------+ +------------------+ |
|
|
| | | ingest |<--------------->| translations | |
|
|
| | | - CSS | Token | - loader | |
|
|
| | | - SCSS | extraction | - registry | |
|
|
| | | - JSON | results | - resolver | |
|
|
| | | - merge | | - merger | |
|
|
| | +-----------+ | - validator | |
|
|
| | ^ | - models | |
|
|
| | | +------------------+ |
|
|
| | | ^ |
|
|
| | +-----------+ | |
|
|
| | | themes |<-----------------------+ |
|
|
| | | - default | Base theme | |
|
|
| | | - light | for merging | |
|
|
| | | - dark | | |
|
|
| | +-----------+ | |
|
|
| | ^ | |
|
|
| | | | |
|
|
| | +-----------+ | |
|
|
| | | models |<-----------------------+ |
|
|
| | | - Theme | Data structures | |
|
|
| | | - Token | & types | |
|
|
| | +-----------+ | |
|
|
| | ^ | |
|
|
| | | | |
|
|
| | +-----------+ | |
|
|
| | | validators|<-----------------------+ |
|
|
| | | - schema | Validation | |
|
|
| | | - project | utilities | |
|
|
| | +-----------+ | |
|
|
| | ^ | |
|
|
| | | v |
|
|
| | +-----------+ +------------------+ |
|
|
| | | storybook |<--------------->| MCP Plugin | |
|
|
| | | - scanner | Theme | (tools/dss_mcp) | |
|
|
| | | - theme | generation | | |
|
|
| | +-----------+ +------------------+ |
|
|
| | |
|
|
| +-----------------------------------------------------------+
|
|
| |
|
|
+-------------------------------------------------------------+
|
|
```
|
|
|
|
---
|
|
|
|
## 2. File Structure
|
|
|
|
### 2.1 Complete Module Structure
|
|
|
|
```
|
|
dss-mvp1/dss/translations/
|
|
|
|
|
+-- __init__.py # Module exports
|
|
+-- models.py # Pydantic data models
|
|
+-- loader.py # Dictionary file loading
|
|
+-- registry.py # In-memory translation registry
|
|
+-- resolver.py # Token path resolution
|
|
+-- merger.py # Theme + custom props merging
|
|
+-- validator.py # Schema & semantic validation
|
|
+-- writer.py # Dictionary file writing
|
|
+-- utils.py # Utility functions
|
|
+-- canonical.py # DSS canonical structure definitions
|
|
|
|
|
+-- schemas/ # JSON Schema files
|
|
| +-- translation-v1.schema.json
|
|
| +-- config.schema.json
|
|
|
|
|
+-- presets/ # Pre-built translation dictionaries
|
|
+-- heroui.json # HeroUI -> DSS mappings
|
|
+-- shadcn.json # shadcn -> DSS mappings
|
|
+-- tailwind.json # Tailwind -> DSS mappings
|
|
```
|
|
|
|
### 2.2 Project `.dss` Structure
|
|
|
|
```
|
|
project-root/
|
|
|
|
|
+-- .dss/ # DSS project configuration
|
|
|
|
|
+-- config.json # Project configuration
|
|
|
|
|
+-- translations/ # Translation dictionaries
|
|
| +-- figma.json # Figma source mappings
|
|
| +-- legacy-css.json # Legacy CSS mappings
|
|
| +-- custom.json # Custom props for this project
|
|
|
|
|
+-- cache/ # Computed/resolved cache
|
|
+-- resolved-theme.json
|
|
+-- token-map.json
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Data Models
|
|
|
|
### 3.1 `models.py` - Core Data Models
|
|
|
|
```python
|
|
"""
|
|
Translation Dictionary Data Models
|
|
|
|
Pydantic models for translation dictionary system.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Any, Dict, List, Optional, Union
|
|
from uuid import uuid4
|
|
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
|
|
|
|
|
class TranslationSource(str, Enum):
|
|
"""Source types for translation dictionaries."""
|
|
FIGMA = "figma"
|
|
CSS = "css"
|
|
SCSS = "scss"
|
|
HEROUI = "heroui"
|
|
SHADCN = "shadcn"
|
|
TAILWIND = "tailwind"
|
|
JSON = "json"
|
|
CUSTOM = "custom"
|
|
|
|
|
|
class MappingType(str, Enum):
|
|
"""Types of mappings in a translation dictionary."""
|
|
TOKEN = "token"
|
|
COMPONENT = "component"
|
|
PATTERN = "pattern"
|
|
|
|
|
|
class TokenMapping(BaseModel):
|
|
"""Single token mapping from source to DSS canonical."""
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
source_token: str = Field(
|
|
...,
|
|
description="Source token name (e.g., '--brand-blue', '$primary-color')"
|
|
)
|
|
dss_token: str = Field(
|
|
...,
|
|
description="DSS canonical token path (e.g., 'color.primary.500')"
|
|
)
|
|
source_value: Optional[str] = Field(
|
|
None,
|
|
description="Original value from source (for reference)"
|
|
)
|
|
notes: Optional[str] = Field(
|
|
None,
|
|
description="Human-readable notes about this mapping"
|
|
)
|
|
confidence: float = Field(
|
|
default=1.0,
|
|
ge=0.0,
|
|
le=1.0,
|
|
description="Confidence score for auto-generated mappings"
|
|
)
|
|
auto_generated: bool = Field(
|
|
default=False,
|
|
description="Whether this mapping was auto-generated"
|
|
)
|
|
|
|
|
|
class ComponentMapping(BaseModel):
|
|
"""Single component mapping from source to DSS canonical."""
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
source_component: str = Field(
|
|
...,
|
|
description="Source component (e.g., '.btn-primary', 'HeroButton')"
|
|
)
|
|
dss_component: str = Field(
|
|
...,
|
|
description="DSS canonical component (e.g., 'Button[variant=primary]')"
|
|
)
|
|
prop_mappings: Dict[str, str] = Field(
|
|
default_factory=dict,
|
|
description="Prop name mappings (source -> DSS)"
|
|
)
|
|
notes: Optional[str] = Field(None)
|
|
|
|
|
|
class PatternMapping(BaseModel):
|
|
"""Pattern mapping for structural translations."""
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
source_pattern: str = Field(
|
|
...,
|
|
description="Source pattern (e.g., 'form-row', 'card-grid')"
|
|
)
|
|
dss_pattern: str = Field(
|
|
...,
|
|
description="DSS canonical pattern"
|
|
)
|
|
notes: Optional[str] = Field(None)
|
|
|
|
|
|
class CustomProp(BaseModel):
|
|
"""Custom property not in DSS core."""
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
name: str = Field(
|
|
...,
|
|
description="Token name in DSS namespace (e.g., 'color.brand.acme.primary')"
|
|
)
|
|
value: Any = Field(
|
|
...,
|
|
description="Token value"
|
|
)
|
|
type: str = Field(
|
|
default="string",
|
|
description="Value type (color, dimension, string, etc.)"
|
|
)
|
|
description: Optional[str] = Field(None)
|
|
deprecated: bool = Field(default=False)
|
|
deprecated_message: Optional[str] = Field(None)
|
|
|
|
|
|
class TranslationMappings(BaseModel):
|
|
"""Container for all mapping types."""
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
tokens: Dict[str, str] = Field(
|
|
default_factory=dict,
|
|
description="Token mappings: source_token -> dss_token"
|
|
)
|
|
components: Dict[str, str] = Field(
|
|
default_factory=dict,
|
|
description="Component mappings: source_component -> dss_component"
|
|
)
|
|
patterns: Dict[str, str] = Field(
|
|
default_factory=dict,
|
|
description="Pattern mappings: source_pattern -> dss_pattern"
|
|
)
|
|
|
|
|
|
class TranslationDictionary(BaseModel):
|
|
"""Complete translation dictionary for a project."""
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
# Metadata
|
|
schema_version: str = Field(
|
|
default="dss-translation-v1",
|
|
alias="$schema",
|
|
description="Schema version identifier"
|
|
)
|
|
uuid: str = Field(
|
|
default_factory=lambda: str(uuid4()),
|
|
description="Unique identifier for this dictionary"
|
|
)
|
|
project: str = Field(
|
|
...,
|
|
description="Project identifier"
|
|
)
|
|
source: TranslationSource = Field(
|
|
...,
|
|
description="Source type for this dictionary"
|
|
)
|
|
version: str = Field(
|
|
default="1.0.0",
|
|
description="Dictionary version"
|
|
)
|
|
created_at: datetime = Field(
|
|
default_factory=datetime.utcnow
|
|
)
|
|
updated_at: datetime = Field(
|
|
default_factory=datetime.utcnow
|
|
)
|
|
|
|
# Mappings
|
|
mappings: TranslationMappings = Field(
|
|
default_factory=TranslationMappings,
|
|
description="All mappings from source to DSS"
|
|
)
|
|
|
|
# Custom extensions
|
|
custom_props: Dict[str, Any] = Field(
|
|
default_factory=dict,
|
|
description="Custom props not in DSS core (namespaced)"
|
|
)
|
|
|
|
# Tracking
|
|
unmapped: List[str] = Field(
|
|
default_factory=list,
|
|
description="Source tokens that couldn't be mapped"
|
|
)
|
|
notes: List[str] = Field(
|
|
default_factory=list,
|
|
description="Human-readable notes"
|
|
)
|
|
|
|
@field_validator('custom_props')
|
|
@classmethod
|
|
def validate_custom_props_namespace(cls, v: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Ensure custom props use proper namespacing."""
|
|
for key in v.keys():
|
|
# Custom props should be namespaced (e.g., color.brand.acme.primary)
|
|
if not '.' in key:
|
|
raise ValueError(
|
|
f"Custom prop '{key}' must use dot-notation namespace "
|
|
"(e.g., 'color.brand.project.name')"
|
|
)
|
|
return v
|
|
|
|
|
|
class TranslationRegistry(BaseModel):
|
|
"""In-memory registry of all loaded translation dictionaries."""
|
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
|
|
dictionaries: Dict[str, TranslationDictionary] = Field(
|
|
default_factory=dict,
|
|
description="Loaded dictionaries by source type"
|
|
)
|
|
combined_token_map: Dict[str, str] = Field(
|
|
default_factory=dict,
|
|
description="Combined source->DSS token mappings"
|
|
)
|
|
combined_component_map: Dict[str, str] = Field(
|
|
default_factory=dict,
|
|
description="Combined source->DSS component mappings"
|
|
)
|
|
all_custom_props: Dict[str, Any] = Field(
|
|
default_factory=dict,
|
|
description="Merged custom props from all dictionaries"
|
|
)
|
|
conflicts: List[Dict[str, Any]] = Field(
|
|
default_factory=list,
|
|
description="Detected mapping conflicts"
|
|
)
|
|
|
|
|
|
class ResolvedToken(BaseModel):
|
|
"""A fully resolved token with provenance."""
|
|
model_config = ConfigDict(extra="forbid")
|
|
|
|
dss_path: str = Field(
|
|
...,
|
|
description="DSS canonical path (e.g., 'color.primary.500')"
|
|
)
|
|
value: Any = Field(
|
|
...,
|
|
description="Resolved value"
|
|
)
|
|
source_token: Optional[str] = Field(
|
|
None,
|
|
description="Original source token if translated"
|
|
)
|
|
source_type: Optional[TranslationSource] = Field(
|
|
None,
|
|
description="Source type if translated"
|
|
)
|
|
is_custom: bool = Field(
|
|
default=False,
|
|
description="Whether this is a custom prop"
|
|
)
|
|
provenance: List[str] = Field(
|
|
default_factory=list,
|
|
description="Resolution chain for debugging"
|
|
)
|
|
|
|
|
|
class ResolvedTheme(BaseModel):
|
|
"""Fully resolved theme with all translations applied."""
|
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
|
|
name: str
|
|
version: str = "1.0.0"
|
|
base_theme: str = Field(
|
|
...,
|
|
description="Base theme name (light/dark)"
|
|
)
|
|
tokens: Dict[str, ResolvedToken] = Field(
|
|
default_factory=dict
|
|
)
|
|
custom_props: Dict[str, ResolvedToken] = Field(
|
|
default_factory=dict
|
|
)
|
|
translations_applied: List[str] = Field(
|
|
default_factory=list,
|
|
description="List of translation dictionaries applied"
|
|
)
|
|
resolved_at: datetime = Field(
|
|
default_factory=datetime.utcnow
|
|
)
|
|
```
|
|
|
|
### 3.2 JSON Schema - `translation-v1.schema.json`
|
|
|
|
```json
|
|
{
|
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
"$id": "https://dss.dev/schemas/translation-v1.schema.json",
|
|
"title": "DSS Translation Dictionary",
|
|
"description": "Schema for DSS translation dictionary files",
|
|
"type": "object",
|
|
"required": ["$schema", "project", "source"],
|
|
"properties": {
|
|
"$schema": {
|
|
"type": "string",
|
|
"const": "dss-translation-v1",
|
|
"description": "Schema version identifier"
|
|
},
|
|
"project": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Project identifier"
|
|
},
|
|
"source": {
|
|
"type": "string",
|
|
"enum": ["figma", "css", "scss", "heroui", "shadcn", "tailwind", "json", "custom"],
|
|
"description": "Source type for this dictionary"
|
|
},
|
|
"version": {
|
|
"type": "string",
|
|
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
|
"default": "1.0.0",
|
|
"description": "Semantic version"
|
|
},
|
|
"mappings": {
|
|
"type": "object",
|
|
"properties": {
|
|
"tokens": {
|
|
"type": "object",
|
|
"additionalProperties": {
|
|
"type": "string",
|
|
"pattern": "^[a-z][a-z0-9]*\\.[a-z][a-z0-9]*(\\.[a-z0-9]+)*$"
|
|
},
|
|
"description": "Token mappings: source -> DSS canonical path"
|
|
},
|
|
"components": {
|
|
"type": "object",
|
|
"additionalProperties": {
|
|
"type": "string"
|
|
},
|
|
"description": "Component mappings"
|
|
},
|
|
"patterns": {
|
|
"type": "object",
|
|
"additionalProperties": {
|
|
"type": "string"
|
|
},
|
|
"description": "Pattern mappings"
|
|
}
|
|
},
|
|
"additionalProperties": false
|
|
},
|
|
"custom_props": {
|
|
"type": "object",
|
|
"propertyNames": {
|
|
"pattern": "^[a-z][a-z0-9]*\\.[a-z][a-z0-9]*(\\.[a-z0-9]+)*$"
|
|
},
|
|
"description": "Custom properties in DSS namespace"
|
|
},
|
|
"unmapped": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
},
|
|
"description": "Source tokens that couldn't be mapped"
|
|
},
|
|
"notes": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string"
|
|
},
|
|
"description": "Human-readable notes"
|
|
}
|
|
},
|
|
"additionalProperties": false
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Core Classes & Methods
|
|
|
|
### 4.1 `loader.py` - Dictionary Loader
|
|
|
|
```python
|
|
"""
|
|
Translation Dictionary Loader
|
|
|
|
Loads and parses translation dictionaries from project .dss directory.
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Union
|
|
from .models import TranslationDictionary, TranslationSource, TranslationRegistry
|
|
from .validator import TranslationValidator
|
|
|
|
|
|
class TranslationDictionaryLoader:
|
|
"""
|
|
Loads translation dictionaries from project .dss/translations/ directory.
|
|
|
|
Usage:
|
|
loader = TranslationDictionaryLoader("/path/to/project")
|
|
registry = await loader.load_all()
|
|
|
|
# Or load specific dictionary
|
|
figma_dict = await loader.load_dictionary("figma")
|
|
"""
|
|
|
|
DEFAULT_DIR = ".dss/translations"
|
|
|
|
def __init__(
|
|
self,
|
|
project_path: Union[str, Path],
|
|
translations_dir: Optional[str] = None,
|
|
validate: bool = True
|
|
):
|
|
"""
|
|
Initialize loader.
|
|
|
|
Args:
|
|
project_path: Root path to project
|
|
translations_dir: Custom translations directory (default: .dss/translations)
|
|
validate: Whether to validate dictionaries on load
|
|
"""
|
|
self.project_path = Path(project_path).resolve()
|
|
self.translations_dir = self.project_path / (
|
|
translations_dir or self.DEFAULT_DIR
|
|
)
|
|
self.validate = validate
|
|
self.validator = TranslationValidator() if validate else None
|
|
|
|
async def load_all(self) -> TranslationRegistry:
|
|
"""
|
|
Load all translation dictionaries from project.
|
|
|
|
Returns:
|
|
TranslationRegistry with all loaded dictionaries
|
|
"""
|
|
registry = TranslationRegistry()
|
|
|
|
if not self.translations_dir.exists():
|
|
return registry
|
|
|
|
for json_file in self.translations_dir.glob("*.json"):
|
|
try:
|
|
dictionary = await self.load_dictionary_file(json_file)
|
|
if dictionary:
|
|
registry.dictionaries[dictionary.source.value] = dictionary
|
|
self._merge_to_registry(registry, dictionary)
|
|
except Exception as e:
|
|
# Log error but continue loading other dictionaries
|
|
registry.conflicts.append({
|
|
"file": str(json_file),
|
|
"error": str(e),
|
|
"type": "load_error"
|
|
})
|
|
|
|
return registry
|
|
|
|
async def load_dictionary(
|
|
self,
|
|
source: Union[str, TranslationSource]
|
|
) -> Optional[TranslationDictionary]:
|
|
"""
|
|
Load a specific translation dictionary by source type.
|
|
|
|
Args:
|
|
source: Source type (e.g., "figma", "css", TranslationSource.FIGMA)
|
|
|
|
Returns:
|
|
TranslationDictionary or None if not found
|
|
"""
|
|
if isinstance(source, str):
|
|
source = TranslationSource(source)
|
|
|
|
file_path = self.translations_dir / f"{source.value}.json"
|
|
if not file_path.exists():
|
|
return None
|
|
|
|
return await self.load_dictionary_file(file_path)
|
|
|
|
async def load_dictionary_file(
|
|
self,
|
|
file_path: Union[str, Path]
|
|
) -> Optional[TranslationDictionary]:
|
|
"""
|
|
Load a translation dictionary from a specific file.
|
|
|
|
Args:
|
|
file_path: Path to JSON file
|
|
|
|
Returns:
|
|
TranslationDictionary or None if invalid
|
|
"""
|
|
file_path = Path(file_path)
|
|
if not file_path.exists():
|
|
raise FileNotFoundError(f"Dictionary file not found: {file_path}")
|
|
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
|
|
# Validate if enabled
|
|
if self.validator:
|
|
validation_result = self.validator.validate_dictionary(data)
|
|
if not validation_result.is_valid:
|
|
raise ValueError(
|
|
f"Invalid dictionary {file_path}: "
|
|
f"{[str(e) for e in validation_result.errors]}"
|
|
)
|
|
|
|
return TranslationDictionary(**data)
|
|
|
|
def _merge_to_registry(
|
|
self,
|
|
registry: TranslationRegistry,
|
|
dictionary: TranslationDictionary
|
|
) -> None:
|
|
"""Merge dictionary mappings into registry."""
|
|
# Merge token mappings
|
|
for source_token, dss_token in dictionary.mappings.tokens.items():
|
|
if source_token in registry.combined_token_map:
|
|
existing = registry.combined_token_map[source_token]
|
|
if existing != dss_token:
|
|
registry.conflicts.append({
|
|
"type": "token_conflict",
|
|
"source_token": source_token,
|
|
"existing_mapping": existing,
|
|
"new_mapping": dss_token,
|
|
"source": dictionary.source.value
|
|
})
|
|
continue
|
|
registry.combined_token_map[source_token] = dss_token
|
|
|
|
# Merge component mappings
|
|
for source_comp, dss_comp in dictionary.mappings.components.items():
|
|
if source_comp in registry.combined_component_map:
|
|
existing = registry.combined_component_map[source_comp]
|
|
if existing != dss_comp:
|
|
registry.conflicts.append({
|
|
"type": "component_conflict",
|
|
"source_component": source_comp,
|
|
"existing_mapping": existing,
|
|
"new_mapping": dss_comp,
|
|
"source": dictionary.source.value
|
|
})
|
|
continue
|
|
registry.combined_component_map[source_comp] = dss_comp
|
|
|
|
# Merge custom props
|
|
for prop_name, prop_value in dictionary.custom_props.items():
|
|
if prop_name in registry.all_custom_props:
|
|
existing = registry.all_custom_props[prop_name]
|
|
if existing != prop_value:
|
|
registry.conflicts.append({
|
|
"type": "custom_prop_conflict",
|
|
"prop_name": prop_name,
|
|
"existing_value": existing,
|
|
"new_value": prop_value,
|
|
"source": dictionary.source.value
|
|
})
|
|
continue
|
|
registry.all_custom_props[prop_name] = prop_value
|
|
|
|
def get_translations_dir(self) -> Path:
|
|
"""Get the translations directory path."""
|
|
return self.translations_dir
|
|
|
|
def has_translations(self) -> bool:
|
|
"""Check if project has any translation dictionaries."""
|
|
if not self.translations_dir.exists():
|
|
return False
|
|
return any(self.translations_dir.glob("*.json"))
|
|
|
|
def list_available_dictionaries(self) -> List[str]:
|
|
"""List available dictionary source types."""
|
|
if not self.translations_dir.exists():
|
|
return []
|
|
return [
|
|
f.stem for f in self.translations_dir.glob("*.json")
|
|
]
|
|
```
|
|
|
|
### 4.2 `resolver.py` - Token Resolution
|
|
|
|
```python
|
|
"""
|
|
Token Resolver
|
|
|
|
Resolves tokens between source formats and DSS canonical structure.
|
|
Supports bidirectional translation.
|
|
"""
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
from .models import (
|
|
TranslationRegistry,
|
|
TranslationSource,
|
|
ResolvedToken,
|
|
)
|
|
from .canonical import DSS_CANONICAL_TOKENS
|
|
|
|
|
|
class TokenResolver:
|
|
"""
|
|
Resolves tokens between source and DSS canonical formats.
|
|
|
|
Supports:
|
|
- Source -> DSS translation (forward)
|
|
- DSS -> Source translation (reverse)
|
|
- Token path resolution with aliasing
|
|
- Reference chain resolution
|
|
|
|
Usage:
|
|
resolver = TokenResolver(registry)
|
|
|
|
# Forward translation
|
|
dss_token = resolver.resolve_to_dss("--brand-blue")
|
|
# -> "color.primary.500"
|
|
|
|
# Reverse translation
|
|
source_token = resolver.resolve_to_source("color.primary.500", "css")
|
|
# -> "--brand-blue"
|
|
"""
|
|
|
|
def __init__(self, registry: TranslationRegistry):
|
|
"""
|
|
Initialize resolver with translation registry.
|
|
|
|
Args:
|
|
registry: Loaded TranslationRegistry with mappings
|
|
"""
|
|
self.registry = registry
|
|
self._reverse_map: Dict[str, Dict[str, str]] = {}
|
|
self._build_reverse_maps()
|
|
|
|
def _build_reverse_maps(self) -> None:
|
|
"""Build reverse lookup maps (DSS -> source) for each source type."""
|
|
for source_type, dictionary in self.registry.dictionaries.items():
|
|
self._reverse_map[source_type] = {
|
|
dss: source
|
|
for source, dss in dictionary.mappings.tokens.items()
|
|
}
|
|
|
|
def resolve_to_dss(
|
|
self,
|
|
source_token: str,
|
|
source_type: Optional[Union[str, TranslationSource]] = None
|
|
) -> Optional[str]:
|
|
"""
|
|
Resolve source token to DSS canonical path.
|
|
|
|
Args:
|
|
source_token: Source token (e.g., "--brand-blue", "$primary")
|
|
source_type: Optional source type hint (searches all if not provided)
|
|
|
|
Returns:
|
|
DSS canonical path or None if not found
|
|
"""
|
|
# Direct lookup in combined map
|
|
if source_token in self.registry.combined_token_map:
|
|
return self.registry.combined_token_map[source_token]
|
|
|
|
# If source type specified, look only there
|
|
if source_type:
|
|
if isinstance(source_type, str):
|
|
source_type = TranslationSource(source_type)
|
|
dictionary = self.registry.dictionaries.get(source_type.value)
|
|
if dictionary:
|
|
return dictionary.mappings.tokens.get(source_token)
|
|
|
|
# Try normalization patterns
|
|
normalized = self._normalize_token_name(source_token)
|
|
return self.registry.combined_token_map.get(normalized)
|
|
|
|
def resolve_to_source(
|
|
self,
|
|
dss_token: str,
|
|
source_type: Union[str, TranslationSource]
|
|
) -> Optional[str]:
|
|
"""
|
|
Resolve DSS token to source format (reverse translation).
|
|
|
|
Args:
|
|
dss_token: DSS canonical path (e.g., "color.primary.500")
|
|
source_type: Target source type
|
|
|
|
Returns:
|
|
Source token name or None if not mapped
|
|
"""
|
|
if isinstance(source_type, str):
|
|
source_type_str = source_type
|
|
else:
|
|
source_type_str = source_type.value
|
|
|
|
reverse_map = self._reverse_map.get(source_type_str, {})
|
|
return reverse_map.get(dss_token)
|
|
|
|
def resolve_token_value(
|
|
self,
|
|
source_token: str,
|
|
base_theme_tokens: Dict[str, Any],
|
|
source_type: Optional[Union[str, TranslationSource]] = None
|
|
) -> Optional[ResolvedToken]:
|
|
"""
|
|
Fully resolve a source token to its DSS value.
|
|
|
|
Args:
|
|
source_token: Source token name
|
|
base_theme_tokens: Base theme token values
|
|
source_type: Optional source type hint
|
|
|
|
Returns:
|
|
ResolvedToken with full provenance or None
|
|
"""
|
|
# Get DSS path
|
|
dss_path = self.resolve_to_dss(source_token, source_type)
|
|
if not dss_path:
|
|
# Check if it's a custom prop
|
|
if source_token in self.registry.all_custom_props:
|
|
return ResolvedToken(
|
|
dss_path=source_token,
|
|
value=self.registry.all_custom_props[source_token],
|
|
source_token=source_token,
|
|
is_custom=True,
|
|
provenance=[f"custom_prop: {source_token}"]
|
|
)
|
|
return None
|
|
|
|
# Resolve value from base theme
|
|
value = self._get_token_value(dss_path, base_theme_tokens)
|
|
|
|
# Determine source type if not provided
|
|
resolved_source = source_type
|
|
if resolved_source is None:
|
|
for src_type, dictionary in self.registry.dictionaries.items():
|
|
if source_token in dictionary.mappings.tokens:
|
|
resolved_source = TranslationSource(src_type)
|
|
break
|
|
|
|
return ResolvedToken(
|
|
dss_path=dss_path,
|
|
value=value,
|
|
source_token=source_token,
|
|
source_type=resolved_source if isinstance(resolved_source, TranslationSource) else (
|
|
TranslationSource(resolved_source) if resolved_source else None
|
|
),
|
|
is_custom=False,
|
|
provenance=[
|
|
f"source: {source_token}",
|
|
f"mapped_to: {dss_path}",
|
|
f"value: {value}"
|
|
]
|
|
)
|
|
|
|
def resolve_all_mappings(
|
|
self,
|
|
base_theme_tokens: Dict[str, Any]
|
|
) -> Dict[str, ResolvedToken]:
|
|
"""
|
|
Resolve all mapped tokens to their DSS values.
|
|
|
|
Args:
|
|
base_theme_tokens: Base theme token values
|
|
|
|
Returns:
|
|
Dict of DSS path -> ResolvedToken
|
|
"""
|
|
resolved = {}
|
|
|
|
# Resolve all mapped tokens
|
|
for source_token, dss_path in self.registry.combined_token_map.items():
|
|
value = self._get_token_value(dss_path, base_theme_tokens)
|
|
|
|
# Find source type
|
|
source_type = None
|
|
for src_type, dictionary in self.registry.dictionaries.items():
|
|
if source_token in dictionary.mappings.tokens:
|
|
source_type = TranslationSource(src_type)
|
|
break
|
|
|
|
resolved[dss_path] = ResolvedToken(
|
|
dss_path=dss_path,
|
|
value=value,
|
|
source_token=source_token,
|
|
source_type=source_type,
|
|
is_custom=False,
|
|
provenance=[f"source: {source_token}", f"mapped_to: {dss_path}"]
|
|
)
|
|
|
|
# Add custom props
|
|
for prop_name, prop_value in self.registry.all_custom_props.items():
|
|
resolved[prop_name] = ResolvedToken(
|
|
dss_path=prop_name,
|
|
value=prop_value,
|
|
is_custom=True,
|
|
provenance=[f"custom_prop: {prop_name}"]
|
|
)
|
|
|
|
return resolved
|
|
|
|
def _get_token_value(
|
|
self,
|
|
dss_path: str,
|
|
base_tokens: Dict[str, Any]
|
|
) -> Any:
|
|
"""Get token value from base theme using DSS path."""
|
|
# Handle nested paths (e.g., "color.primary.500")
|
|
parts = dss_path.split('.')
|
|
current = base_tokens
|
|
|
|
for part in parts:
|
|
if isinstance(current, dict):
|
|
current = current.get(part)
|
|
if current is None:
|
|
break
|
|
else:
|
|
return None
|
|
|
|
# If we got a DesignToken object, extract value
|
|
if hasattr(current, 'value'):
|
|
return current.value
|
|
|
|
return current
|
|
|
|
def _normalize_token_name(self, token: str) -> str:
|
|
"""Normalize token name for lookup."""
|
|
# Remove common prefixes
|
|
normalized = token.lstrip('-$')
|
|
|
|
# Convert various formats to dot notation
|
|
normalized = normalized.replace('-', '.').replace('_', '.')
|
|
|
|
# Handle var() references
|
|
if normalized.startswith('var(') and normalized.endswith(')'):
|
|
normalized = normalized[4:-1].lstrip('-')
|
|
|
|
return normalized.lower()
|
|
|
|
def get_unmapped_tokens(self) -> List[str]:
|
|
"""Get list of tokens that couldn't be mapped."""
|
|
unmapped = []
|
|
for dictionary in self.registry.dictionaries.values():
|
|
unmapped.extend(dictionary.unmapped)
|
|
return list(set(unmapped))
|
|
|
|
def validate_dss_path(self, path: str) -> bool:
|
|
"""Validate that a path matches DSS canonical structure."""
|
|
return path in DSS_CANONICAL_TOKENS or self._is_valid_custom_namespace(path)
|
|
|
|
def _is_valid_custom_namespace(self, path: str) -> bool:
|
|
"""Check if path uses valid custom namespace."""
|
|
parts = path.split('.')
|
|
if len(parts) < 3:
|
|
return False
|
|
# Custom props should be like: color.brand.acme.primary
|
|
return parts[1] == 'brand' or parts[1] == 'custom'
|
|
```
|
|
|
|
### 4.3 `merger.py` - Theme Merger
|
|
|
|
```python
|
|
"""
|
|
Theme Merger
|
|
|
|
Merges base DSS theme with translation mappings and custom props.
|
|
"""
|
|
|
|
from typing import Any, Dict, List, Optional, Union
|
|
from datetime import datetime
|
|
from .models import (
|
|
TranslationRegistry,
|
|
ResolvedToken,
|
|
ResolvedTheme,
|
|
)
|
|
from .resolver import TokenResolver
|
|
from dss.models.theme import Theme, DesignToken, TokenCategory
|
|
from dss.themes.default_themes import get_default_light_theme, get_default_dark_theme
|
|
|
|
|
|
class ThemeMerger:
|
|
"""
|
|
Merges base DSS theme with project-specific customizations.
|
|
|
|
The merge hierarchy:
|
|
1. Base Theme (DSS Light or Dark)
|
|
2. Translation Mappings (external tokens -> DSS)
|
|
3. Custom Props (project-specific extensions)
|
|
|
|
Usage:
|
|
merger = ThemeMerger(registry)
|
|
resolved = await merger.merge(base_theme="light")
|
|
"""
|
|
|
|
def __init__(self, registry: TranslationRegistry):
|
|
"""
|
|
Initialize merger with translation registry.
|
|
|
|
Args:
|
|
registry: TranslationRegistry with loaded dictionaries
|
|
"""
|
|
self.registry = registry
|
|
self.resolver = TokenResolver(registry)
|
|
|
|
async def merge(
|
|
self,
|
|
base_theme: str = "light",
|
|
project_name: Optional[str] = None
|
|
) -> ResolvedTheme:
|
|
"""
|
|
Merge base theme with translations and custom props.
|
|
|
|
Args:
|
|
base_theme: Base theme name ("light" or "dark")
|
|
project_name: Project name for resolved theme
|
|
|
|
Returns:
|
|
ResolvedTheme with all tokens resolved
|
|
"""
|
|
# Get base theme
|
|
if base_theme == "light":
|
|
theme = get_default_light_theme()
|
|
elif base_theme == "dark":
|
|
theme = get_default_dark_theme()
|
|
else:
|
|
raise ValueError(f"Unknown base theme: {base_theme}")
|
|
|
|
# Convert theme tokens to dict for resolution
|
|
base_tokens = self._theme_to_dict(theme)
|
|
|
|
# Resolve all mapped tokens
|
|
resolved_tokens = self.resolver.resolve_all_mappings(base_tokens)
|
|
|
|
# Separate core tokens from custom props
|
|
core_tokens = {}
|
|
custom_props = {}
|
|
|
|
for dss_path, resolved in resolved_tokens.items():
|
|
if resolved.is_custom:
|
|
custom_props[dss_path] = resolved
|
|
else:
|
|
core_tokens[dss_path] = resolved
|
|
|
|
# Add base theme tokens that aren't in mappings
|
|
for token_name, token in theme.tokens.items():
|
|
# Normalize token name to DSS path
|
|
dss_path = self._normalize_to_dss_path(token_name)
|
|
if dss_path not in core_tokens:
|
|
core_tokens[dss_path] = ResolvedToken(
|
|
dss_path=dss_path,
|
|
value=token.value,
|
|
is_custom=False,
|
|
provenance=[f"base_theme: {base_theme}"]
|
|
)
|
|
|
|
return ResolvedTheme(
|
|
name=project_name or f"resolved-{base_theme}",
|
|
version="1.0.0",
|
|
base_theme=base_theme,
|
|
tokens=core_tokens,
|
|
custom_props=custom_props,
|
|
translations_applied=[
|
|
dict_name for dict_name in self.registry.dictionaries.keys()
|
|
],
|
|
resolved_at=datetime.utcnow()
|
|
)
|
|
|
|
def _theme_to_dict(self, theme: Theme) -> Dict[str, Any]:
|
|
"""Convert Theme object to nested dict for resolution."""
|
|
result = {}
|
|
for token_name, token in theme.tokens.items():
|
|
# Convert flat token names to nested structure
|
|
parts = self._normalize_to_dss_path(token_name).split('.')
|
|
current = result
|
|
for part in parts[:-1]:
|
|
if part not in current:
|
|
current[part] = {}
|
|
current = current[part]
|
|
current[parts[-1]] = token.value
|
|
return result
|
|
|
|
def _normalize_to_dss_path(self, token_name: str) -> str:
|
|
"""Normalize token name to DSS canonical path."""
|
|
# Handle various formats
|
|
normalized = token_name.replace('-', '.').replace('_', '.')
|
|
|
|
# Map common prefixes
|
|
prefix_map = {
|
|
'space.': 'spacing.',
|
|
'radius.': 'border.radius.',
|
|
'text.': 'typography.size.',
|
|
}
|
|
|
|
for old, new in prefix_map.items():
|
|
if normalized.startswith(old):
|
|
normalized = new + normalized[len(old):]
|
|
break
|
|
|
|
return normalized
|
|
|
|
async def merge_custom_props(
|
|
self,
|
|
resolved_theme: ResolvedTheme,
|
|
additional_props: Dict[str, Any]
|
|
) -> ResolvedTheme:
|
|
"""
|
|
Add additional custom props to a resolved theme.
|
|
|
|
Args:
|
|
resolved_theme: Existing resolved theme
|
|
additional_props: Additional custom props to merge
|
|
|
|
Returns:
|
|
Updated ResolvedTheme
|
|
"""
|
|
for prop_name, prop_value in additional_props.items():
|
|
resolved_theme.custom_props[prop_name] = ResolvedToken(
|
|
dss_path=prop_name,
|
|
value=prop_value,
|
|
is_custom=True,
|
|
provenance=["additional_custom_prop"]
|
|
)
|
|
|
|
resolved_theme.resolved_at = datetime.utcnow()
|
|
return resolved_theme
|
|
|
|
def export_as_theme(self, resolved: ResolvedTheme) -> Theme:
|
|
"""
|
|
Convert ResolvedTheme back to Theme model.
|
|
|
|
Args:
|
|
resolved: ResolvedTheme to convert
|
|
|
|
Returns:
|
|
Theme model instance
|
|
"""
|
|
tokens = {}
|
|
|
|
# Add core tokens
|
|
for dss_path, resolved_token in resolved.tokens.items():
|
|
token_name = dss_path.replace('.', '-')
|
|
tokens[token_name] = DesignToken(
|
|
name=token_name,
|
|
value=resolved_token.value,
|
|
type=self._infer_type(dss_path, resolved_token.value),
|
|
category=self._infer_category(dss_path),
|
|
source=f"resolved:{resolved.base_theme}"
|
|
)
|
|
|
|
# Add custom props
|
|
for dss_path, resolved_token in resolved.custom_props.items():
|
|
token_name = dss_path.replace('.', '-')
|
|
tokens[token_name] = DesignToken(
|
|
name=token_name,
|
|
value=resolved_token.value,
|
|
type=self._infer_type(dss_path, resolved_token.value),
|
|
category=TokenCategory.OTHER,
|
|
source="custom_prop"
|
|
)
|
|
|
|
return Theme(
|
|
name=resolved.name,
|
|
version=resolved.version,
|
|
tokens=tokens
|
|
)
|
|
|
|
def _infer_type(self, path: str, value: Any) -> str:
|
|
"""Infer token type from path and value."""
|
|
if 'color' in path:
|
|
return 'color'
|
|
if 'spacing' in path or 'size' in path or 'radius' in path:
|
|
return 'dimension'
|
|
if 'font' in path:
|
|
return 'typography'
|
|
if 'shadow' in path:
|
|
return 'shadow'
|
|
return 'string'
|
|
|
|
def _infer_category(self, path: str) -> TokenCategory:
|
|
"""Infer token category from DSS path."""
|
|
if path.startswith('color'):
|
|
return TokenCategory.COLOR
|
|
if path.startswith('spacing'):
|
|
return TokenCategory.SPACING
|
|
if path.startswith('typography') or path.startswith('font'):
|
|
return TokenCategory.TYPOGRAPHY
|
|
if path.startswith('border') or path.startswith('radius'):
|
|
return TokenCategory.RADIUS
|
|
if path.startswith('shadow'):
|
|
return TokenCategory.SHADOW
|
|
return TokenCategory.OTHER
|
|
```
|
|
|
|
### 4.4 `validator.py` - Validation Engine
|
|
|
|
```python
|
|
"""
|
|
Translation Dictionary Validator
|
|
|
|
Validates translation dictionary schema and semantic correctness.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
from pydantic import ValidationError as PydanticValidationError
|
|
from .models import TranslationDictionary, TranslationSource
|
|
from .canonical import DSS_CANONICAL_TOKENS, DSS_CANONICAL_COMPONENTS
|
|
|
|
|
|
class ValidationError:
|
|
"""Single validation error."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
path: Optional[str] = None,
|
|
severity: str = "error"
|
|
):
|
|
self.message = message
|
|
self.path = path
|
|
self.severity = severity # error, warning, info
|
|
|
|
def __str__(self) -> str:
|
|
if self.path:
|
|
return f"[{self.severity}] {self.path}: {self.message}"
|
|
return f"[{self.severity}] {self.message}"
|
|
|
|
|
|
class ValidationResult:
|
|
"""Validation result container."""
|
|
|
|
def __init__(self):
|
|
self.is_valid = True
|
|
self.errors: List[ValidationError] = []
|
|
self.warnings: List[ValidationError] = []
|
|
self.info: List[ValidationError] = []
|
|
|
|
def add_error(self, message: str, path: Optional[str] = None) -> None:
|
|
self.errors.append(ValidationError(message, path, "error"))
|
|
self.is_valid = False
|
|
|
|
def add_warning(self, message: str, path: Optional[str] = None) -> None:
|
|
self.warnings.append(ValidationError(message, path, "warning"))
|
|
|
|
def add_info(self, message: str, path: Optional[str] = None) -> None:
|
|
self.info.append(ValidationError(message, path, "info"))
|
|
|
|
|
|
class TranslationValidator:
|
|
"""
|
|
Validates translation dictionaries.
|
|
|
|
Validation stages:
|
|
1. Schema validation - JSON structure matches Pydantic model
|
|
2. Token path validation - DSS paths are canonical
|
|
3. Component validation - Component mappings are valid
|
|
4. Custom prop validation - Namespacing is correct
|
|
5. Consistency validation - No conflicts or duplicates
|
|
"""
|
|
|
|
# Valid DSS path pattern
|
|
DSS_PATH_PATTERN = re.compile(
|
|
r'^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$'
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
strict: bool = False,
|
|
allow_unknown_tokens: bool = True
|
|
):
|
|
"""
|
|
Initialize validator.
|
|
|
|
Args:
|
|
strict: If True, unknown DSS tokens are errors (not warnings)
|
|
allow_unknown_tokens: If False, all tokens must exist in canonical
|
|
"""
|
|
self.strict = strict
|
|
self.allow_unknown_tokens = allow_unknown_tokens
|
|
|
|
def validate_dictionary(
|
|
self,
|
|
data: Dict[str, Any]
|
|
) -> ValidationResult:
|
|
"""
|
|
Validate a translation dictionary.
|
|
|
|
Args:
|
|
data: Dictionary data to validate
|
|
|
|
Returns:
|
|
ValidationResult with all errors/warnings
|
|
"""
|
|
result = ValidationResult()
|
|
|
|
# Stage 1: Schema validation
|
|
self._validate_schema(data, result)
|
|
if not result.is_valid:
|
|
return result
|
|
|
|
# Stage 2: Token path validation
|
|
self._validate_token_paths(data, result)
|
|
|
|
# Stage 3: Component validation
|
|
self._validate_components(data, result)
|
|
|
|
# Stage 4: Custom prop validation
|
|
self._validate_custom_props(data, result)
|
|
|
|
# Stage 5: Consistency validation
|
|
self._validate_consistency(data, result)
|
|
|
|
return result
|
|
|
|
def _validate_schema(
|
|
self,
|
|
data: Dict[str, Any],
|
|
result: ValidationResult
|
|
) -> None:
|
|
"""Stage 1: Validate JSON structure."""
|
|
try:
|
|
TranslationDictionary(**data)
|
|
except PydanticValidationError as e:
|
|
for error in e.errors():
|
|
path = ".".join(str(loc) for loc in error["loc"])
|
|
result.add_error(error["msg"], path)
|
|
except Exception as e:
|
|
result.add_error(f"Schema validation failed: {str(e)}")
|
|
|
|
def _validate_token_paths(
|
|
self,
|
|
data: Dict[str, Any],
|
|
result: ValidationResult
|
|
) -> None:
|
|
"""Stage 2: Validate DSS token paths."""
|
|
mappings = data.get("mappings", {})
|
|
tokens = mappings.get("tokens", {})
|
|
|
|
for source_token, dss_path in tokens.items():
|
|
# Validate path format
|
|
if not self.DSS_PATH_PATTERN.match(dss_path):
|
|
result.add_error(
|
|
f"Invalid DSS path format: '{dss_path}' "
|
|
"(must be dot-notation like 'color.primary.500')",
|
|
f"mappings.tokens.{source_token}"
|
|
)
|
|
continue
|
|
|
|
# Validate against canonical structure
|
|
if dss_path not in DSS_CANONICAL_TOKENS:
|
|
if self._is_custom_namespace(dss_path):
|
|
# Custom namespaces are allowed
|
|
result.add_info(
|
|
f"Custom namespace token: {dss_path}",
|
|
f"mappings.tokens.{source_token}"
|
|
)
|
|
elif self.allow_unknown_tokens:
|
|
result.add_warning(
|
|
f"DSS token not in canonical structure: {dss_path}",
|
|
f"mappings.tokens.{source_token}"
|
|
)
|
|
else:
|
|
result.add_error(
|
|
f"Unknown DSS token: {dss_path}",
|
|
f"mappings.tokens.{source_token}"
|
|
)
|
|
|
|
def _validate_components(
|
|
self,
|
|
data: Dict[str, Any],
|
|
result: ValidationResult
|
|
) -> None:
|
|
"""Stage 3: Validate component mappings."""
|
|
mappings = data.get("mappings", {})
|
|
components = mappings.get("components", {})
|
|
|
|
for source_comp, dss_comp in components.items():
|
|
# Extract base component name (before any variant specifiers)
|
|
base_comp = dss_comp.split('[')[0]
|
|
|
|
if base_comp not in DSS_CANONICAL_COMPONENTS:
|
|
result.add_warning(
|
|
f"Component not in DSS canonical set: {base_comp}",
|
|
f"mappings.components.{source_comp}"
|
|
)
|
|
|
|
# Validate variant syntax if present
|
|
if '[' in dss_comp:
|
|
if not self._validate_variant_syntax(dss_comp):
|
|
result.add_error(
|
|
f"Invalid variant syntax: {dss_comp}",
|
|
f"mappings.components.{source_comp}"
|
|
)
|
|
|
|
def _validate_custom_props(
|
|
self,
|
|
data: Dict[str, Any],
|
|
result: ValidationResult
|
|
) -> None:
|
|
"""Stage 4: Validate custom prop namespacing."""
|
|
custom_props = data.get("custom_props", {})
|
|
|
|
for prop_name, prop_value in custom_props.items():
|
|
# Must use dot notation
|
|
if '.' not in prop_name:
|
|
result.add_error(
|
|
f"Custom prop must use dot-notation namespace: {prop_name}",
|
|
f"custom_props.{prop_name}"
|
|
)
|
|
continue
|
|
|
|
# Should use brand/custom namespace
|
|
parts = prop_name.split('.')
|
|
if len(parts) >= 2 and parts[1] not in ('brand', 'custom'):
|
|
result.add_warning(
|
|
f"Custom prop should use 'brand' or 'custom' namespace: {prop_name}. "
|
|
f"Recommended: {parts[0]}.brand.{'.'.join(parts[1:])}",
|
|
f"custom_props.{prop_name}"
|
|
)
|
|
|
|
def _validate_consistency(
|
|
self,
|
|
data: Dict[str, Any],
|
|
result: ValidationResult
|
|
) -> None:
|
|
"""Stage 5: Validate internal consistency."""
|
|
mappings = data.get("mappings", {})
|
|
tokens = mappings.get("tokens", {})
|
|
custom_props = data.get("custom_props", {})
|
|
|
|
# Check for duplicate DSS targets
|
|
dss_targets = list(tokens.values())
|
|
seen = set()
|
|
for target in dss_targets:
|
|
if target in seen:
|
|
result.add_warning(
|
|
f"Multiple source tokens map to same DSS token: {target}",
|
|
"mappings.tokens"
|
|
)
|
|
seen.add(target)
|
|
|
|
# Check custom props don't conflict with mappings
|
|
for prop_name in custom_props.keys():
|
|
if prop_name in tokens.values():
|
|
result.add_error(
|
|
f"Custom prop conflicts with mapping target: {prop_name}",
|
|
f"custom_props.{prop_name}"
|
|
)
|
|
|
|
def _is_custom_namespace(self, path: str) -> bool:
|
|
"""Check if path uses custom namespace."""
|
|
parts = path.split('.')
|
|
if len(parts) >= 2:
|
|
return parts[1] in ('brand', 'custom')
|
|
return False
|
|
|
|
def _validate_variant_syntax(self, comp: str) -> bool:
|
|
"""Validate component variant syntax like Button[variant=primary]."""
|
|
if '[' not in comp:
|
|
return True
|
|
|
|
# Check for matching brackets
|
|
if comp.count('[') != comp.count(']'):
|
|
return False
|
|
|
|
# Extract variant part
|
|
variant_match = re.search(r'\[([^\]]+)\]', comp)
|
|
if not variant_match:
|
|
return False
|
|
|
|
# Validate key=value format
|
|
variant_str = variant_match.group(1)
|
|
for pair in variant_str.split(','):
|
|
if '=' not in pair:
|
|
return False
|
|
key, value = pair.split('=', 1)
|
|
if not key.strip() or not value.strip():
|
|
return False
|
|
|
|
return True
|
|
|
|
def validate_file(self, file_path: str) -> ValidationResult:
|
|
"""
|
|
Validate a translation dictionary file.
|
|
|
|
Args:
|
|
file_path: Path to JSON file
|
|
|
|
Returns:
|
|
ValidationResult
|
|
"""
|
|
result = ValidationResult()
|
|
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
except json.JSONDecodeError as e:
|
|
result.add_error(f"Invalid JSON: {str(e)}")
|
|
return result
|
|
except FileNotFoundError:
|
|
result.add_error(f"File not found: {file_path}")
|
|
return result
|
|
|
|
return self.validate_dictionary(data)
|
|
```
|
|
|
|
### 4.5 `canonical.py` - DSS Canonical Definitions
|
|
|
|
```python
|
|
"""
|
|
DSS Canonical Structure Definitions
|
|
|
|
Defines the immutable DSS canonical token and component structure.
|
|
These definitions are used for validation and auto-completion.
|
|
"""
|
|
|
|
from typing import Set, Dict, List
|
|
|
|
# DSS Canonical Token Paths
|
|
# These are the core tokens that DSS defines
|
|
DSS_CANONICAL_TOKENS: Set[str] = {
|
|
# Colors - Primary
|
|
"color.primary.50",
|
|
"color.primary.100",
|
|
"color.primary.200",
|
|
"color.primary.300",
|
|
"color.primary.400",
|
|
"color.primary.500",
|
|
"color.primary.600",
|
|
"color.primary.700",
|
|
"color.primary.800",
|
|
"color.primary.900",
|
|
|
|
# Colors - Secondary
|
|
"color.secondary.50",
|
|
"color.secondary.100",
|
|
"color.secondary.200",
|
|
"color.secondary.300",
|
|
"color.secondary.400",
|
|
"color.secondary.500",
|
|
"color.secondary.600",
|
|
"color.secondary.700",
|
|
"color.secondary.800",
|
|
"color.secondary.900",
|
|
|
|
# Colors - Neutral
|
|
"color.neutral.50",
|
|
"color.neutral.100",
|
|
"color.neutral.200",
|
|
"color.neutral.300",
|
|
"color.neutral.400",
|
|
"color.neutral.500",
|
|
"color.neutral.600",
|
|
"color.neutral.700",
|
|
"color.neutral.800",
|
|
"color.neutral.900",
|
|
|
|
# Colors - Semantic
|
|
"color.success.500",
|
|
"color.warning.500",
|
|
"color.danger.500",
|
|
"color.info.500",
|
|
"color.accent.500",
|
|
|
|
# Colors - Surface
|
|
"color.background",
|
|
"color.foreground",
|
|
"color.muted",
|
|
"color.border",
|
|
"color.ring",
|
|
|
|
# Spacing
|
|
"spacing.xs",
|
|
"spacing.sm",
|
|
"spacing.md",
|
|
"spacing.lg",
|
|
"spacing.xl",
|
|
"spacing.2xl",
|
|
"spacing.base",
|
|
|
|
# Typography - Size
|
|
"typography.size.xs",
|
|
"typography.size.sm",
|
|
"typography.size.base",
|
|
"typography.size.lg",
|
|
"typography.size.xl",
|
|
"typography.size.2xl",
|
|
"typography.size.3xl",
|
|
"typography.size.4xl",
|
|
|
|
# Typography - Weight
|
|
"typography.weight.light",
|
|
"typography.weight.normal",
|
|
"typography.weight.medium",
|
|
"typography.weight.semibold",
|
|
"typography.weight.bold",
|
|
|
|
# Typography - Line Height
|
|
"typography.lineHeight.tight",
|
|
"typography.lineHeight.normal",
|
|
"typography.lineHeight.relaxed",
|
|
|
|
# Typography - Font Family
|
|
"typography.family.sans",
|
|
"typography.family.serif",
|
|
"typography.family.mono",
|
|
|
|
# Border Radius
|
|
"border.radius.none",
|
|
"border.radius.sm",
|
|
"border.radius.md",
|
|
"border.radius.lg",
|
|
"border.radius.xl",
|
|
"border.radius.full",
|
|
|
|
# Border Width
|
|
"border.width.none",
|
|
"border.width.thin",
|
|
"border.width.default",
|
|
"border.width.thick",
|
|
|
|
# Shadows
|
|
"shadow.none",
|
|
"shadow.sm",
|
|
"shadow.md",
|
|
"shadow.lg",
|
|
"shadow.xl",
|
|
"shadow.inner",
|
|
|
|
# Motion - Duration
|
|
"motion.duration.instant",
|
|
"motion.duration.fast",
|
|
"motion.duration.normal",
|
|
"motion.duration.slow",
|
|
|
|
# Motion - Easing
|
|
"motion.easing.linear",
|
|
"motion.easing.ease",
|
|
"motion.easing.easeIn",
|
|
"motion.easing.easeOut",
|
|
"motion.easing.easeInOut",
|
|
|
|
# Z-Index
|
|
"zIndex.base",
|
|
"zIndex.dropdown",
|
|
"zIndex.sticky",
|
|
"zIndex.fixed",
|
|
"zIndex.modal",
|
|
"zIndex.popover",
|
|
"zIndex.tooltip",
|
|
|
|
# Opacity
|
|
"opacity.0",
|
|
"opacity.25",
|
|
"opacity.50",
|
|
"opacity.75",
|
|
"opacity.100",
|
|
|
|
# Breakpoints
|
|
"breakpoint.sm",
|
|
"breakpoint.md",
|
|
"breakpoint.lg",
|
|
"breakpoint.xl",
|
|
"breakpoint.2xl",
|
|
}
|
|
|
|
# Commonly used aliases for DSS tokens
|
|
DSS_TOKEN_ALIASES: Dict[str, str] = {
|
|
# Color aliases
|
|
"color.primary": "color.primary.500",
|
|
"color.secondary": "color.secondary.500",
|
|
"color.success": "color.success.500",
|
|
"color.warning": "color.warning.500",
|
|
"color.danger": "color.danger.500",
|
|
"color.destructive": "color.danger.500",
|
|
"color.error": "color.danger.500",
|
|
|
|
# Spacing aliases
|
|
"space.xs": "spacing.xs",
|
|
"space.sm": "spacing.sm",
|
|
"space.md": "spacing.md",
|
|
"space.lg": "spacing.lg",
|
|
"space.xl": "spacing.xl",
|
|
|
|
# Radius aliases
|
|
"radius.sm": "border.radius.sm",
|
|
"radius.md": "border.radius.md",
|
|
"radius.lg": "border.radius.lg",
|
|
|
|
# Typography aliases
|
|
"font.size.base": "typography.size.base",
|
|
"font.weight.bold": "typography.weight.bold",
|
|
"lineHeight.normal": "typography.lineHeight.normal",
|
|
}
|
|
|
|
# DSS Canonical Components
|
|
DSS_CANONICAL_COMPONENTS: Set[str] = {
|
|
# Primitives
|
|
"Button",
|
|
"Input",
|
|
"Textarea",
|
|
"Select",
|
|
"Checkbox",
|
|
"Radio",
|
|
"RadioGroup",
|
|
"Switch",
|
|
"Slider",
|
|
"Toggle",
|
|
|
|
# Layout
|
|
"Box",
|
|
"Flex",
|
|
"Grid",
|
|
"Container",
|
|
"Stack",
|
|
"Spacer",
|
|
"Divider",
|
|
|
|
# Data Display
|
|
"Card",
|
|
"Avatar",
|
|
"Badge",
|
|
"Chip",
|
|
"Tag",
|
|
"Icon",
|
|
"Image",
|
|
"Table",
|
|
"List",
|
|
"ListItem",
|
|
|
|
# Feedback
|
|
"Alert",
|
|
"Toast",
|
|
"Progress",
|
|
"Spinner",
|
|
"Skeleton",
|
|
"Tooltip",
|
|
|
|
# Overlay
|
|
"Modal",
|
|
"Dialog",
|
|
"Drawer",
|
|
"Popover",
|
|
"Dropdown",
|
|
"DropdownMenu",
|
|
"ContextMenu",
|
|
|
|
# Navigation
|
|
"Tabs",
|
|
"TabList",
|
|
"Tab",
|
|
"TabPanel",
|
|
"Breadcrumb",
|
|
"Pagination",
|
|
"Menu",
|
|
"MenuItem",
|
|
"NavLink",
|
|
"Link",
|
|
|
|
# Typography
|
|
"Text",
|
|
"Heading",
|
|
"Label",
|
|
"Code",
|
|
|
|
# Forms
|
|
"Form",
|
|
"FormControl",
|
|
"FormLabel",
|
|
"FormHelperText",
|
|
"FormErrorMessage",
|
|
}
|
|
|
|
# DSS Component Variants
|
|
DSS_COMPONENT_VARIANTS: Dict[str, List[str]] = {
|
|
"Button": ["variant", "size", "colorScheme", "isDisabled", "isLoading"],
|
|
"Input": ["variant", "size", "isDisabled", "isInvalid", "isReadOnly"],
|
|
"Card": ["variant", "size", "shadow"],
|
|
"Badge": ["variant", "colorScheme", "size"],
|
|
"Alert": ["status", "variant"],
|
|
"Modal": ["size", "isCentered", "scrollBehavior"],
|
|
}
|
|
|
|
# Valid variant values
|
|
DSS_VARIANT_VALUES: Dict[str, Dict[str, List[str]]] = {
|
|
"Button": {
|
|
"variant": ["solid", "outline", "ghost", "link", "unstyled"],
|
|
"size": ["xs", "sm", "md", "lg"],
|
|
"colorScheme": ["primary", "secondary", "success", "warning", "danger"],
|
|
},
|
|
"Input": {
|
|
"variant": ["outline", "filled", "flushed", "unstyled"],
|
|
"size": ["xs", "sm", "md", "lg"],
|
|
},
|
|
"Card": {
|
|
"variant": ["elevated", "outline", "filled", "unstyled"],
|
|
"size": ["sm", "md", "lg"],
|
|
},
|
|
}
|
|
|
|
|
|
def get_canonical_token_categories() -> Dict[str, List[str]]:
|
|
"""Get tokens organized by category."""
|
|
categories: Dict[str, List[str]] = {}
|
|
|
|
for token in DSS_CANONICAL_TOKENS:
|
|
parts = token.split('.')
|
|
category = parts[0]
|
|
if category not in categories:
|
|
categories[category] = []
|
|
categories[category].append(token)
|
|
|
|
return categories
|
|
|
|
|
|
def is_valid_dss_token(path: str) -> bool:
|
|
"""Check if token path is in canonical structure or valid custom namespace."""
|
|
if path in DSS_CANONICAL_TOKENS:
|
|
return True
|
|
|
|
# Check aliases
|
|
if path in DSS_TOKEN_ALIASES:
|
|
return True
|
|
|
|
# Check custom namespace
|
|
parts = path.split('.')
|
|
if len(parts) >= 3 and parts[1] in ('brand', 'custom'):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def resolve_alias(path: str) -> str:
|
|
"""Resolve token alias to canonical path."""
|
|
return DSS_TOKEN_ALIASES.get(path, path)
|
|
```
|
|
|
|
### 4.6 `writer.py` - Dictionary Writer
|
|
|
|
```python
|
|
"""
|
|
Translation Dictionary Writer
|
|
|
|
Writes and updates translation dictionary files.
|
|
"""
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Union
|
|
from .models import TranslationDictionary, TranslationSource, TranslationMappings
|
|
|
|
|
|
class TranslationDictionaryWriter:
|
|
"""
|
|
Writes translation dictionaries to project .dss/translations/ directory.
|
|
|
|
Usage:
|
|
writer = TranslationDictionaryWriter("/path/to/project")
|
|
|
|
# Create new dictionary
|
|
await writer.create(
|
|
source=TranslationSource.CSS,
|
|
project="my-project",
|
|
token_mappings={"--brand-blue": "color.primary.500"}
|
|
)
|
|
|
|
# Add mapping to existing dictionary
|
|
await writer.add_mapping(
|
|
source=TranslationSource.CSS,
|
|
source_token="--brand-green",
|
|
dss_token="color.success.500"
|
|
)
|
|
"""
|
|
|
|
DEFAULT_DIR = ".dss/translations"
|
|
|
|
def __init__(
|
|
self,
|
|
project_path: Union[str, Path],
|
|
translations_dir: Optional[str] = None
|
|
):
|
|
"""
|
|
Initialize writer.
|
|
|
|
Args:
|
|
project_path: Root path to project
|
|
translations_dir: Custom translations directory
|
|
"""
|
|
self.project_path = Path(project_path).resolve()
|
|
self.translations_dir = self.project_path / (
|
|
translations_dir or self.DEFAULT_DIR
|
|
)
|
|
|
|
async def create(
|
|
self,
|
|
source: Union[str, TranslationSource],
|
|
project: str,
|
|
token_mappings: Optional[Dict[str, str]] = None,
|
|
component_mappings: Optional[Dict[str, str]] = None,
|
|
custom_props: Optional[Dict[str, Any]] = None,
|
|
notes: Optional[List[str]] = None
|
|
) -> TranslationDictionary:
|
|
"""
|
|
Create a new translation dictionary.
|
|
|
|
Args:
|
|
source: Source type
|
|
project: Project identifier
|
|
token_mappings: Initial token mappings
|
|
component_mappings: Initial component mappings
|
|
custom_props: Initial custom props
|
|
notes: Optional notes
|
|
|
|
Returns:
|
|
Created TranslationDictionary
|
|
"""
|
|
if isinstance(source, str):
|
|
source = TranslationSource(source)
|
|
|
|
# Ensure directory exists
|
|
self.translations_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create dictionary
|
|
dictionary = TranslationDictionary(
|
|
project=project,
|
|
source=source,
|
|
mappings=TranslationMappings(
|
|
tokens=token_mappings or {},
|
|
components=component_mappings or {},
|
|
),
|
|
custom_props=custom_props or {},
|
|
notes=notes or []
|
|
)
|
|
|
|
# Write to file
|
|
file_path = self.translations_dir / f"{source.value}.json"
|
|
await self._write_file(file_path, dictionary)
|
|
|
|
return dictionary
|
|
|
|
async def update(
|
|
self,
|
|
source: Union[str, TranslationSource],
|
|
token_mappings: Optional[Dict[str, str]] = None,
|
|
component_mappings: Optional[Dict[str, str]] = None,
|
|
custom_props: Optional[Dict[str, Any]] = None,
|
|
notes: Optional[List[str]] = None
|
|
) -> TranslationDictionary:
|
|
"""
|
|
Update an existing translation dictionary.
|
|
|
|
Args:
|
|
source: Source type
|
|
token_mappings: Token mappings to add/update
|
|
component_mappings: Component mappings to add/update
|
|
custom_props: Custom props to add/update
|
|
notes: Notes to append
|
|
|
|
Returns:
|
|
Updated TranslationDictionary
|
|
"""
|
|
if isinstance(source, str):
|
|
source = TranslationSource(source)
|
|
|
|
file_path = self.translations_dir / f"{source.value}.json"
|
|
if not file_path.exists():
|
|
raise FileNotFoundError(
|
|
f"Dictionary not found: {file_path}. Use create() first."
|
|
)
|
|
|
|
# Load existing
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
|
|
dictionary = TranslationDictionary(**data)
|
|
|
|
# Update mappings
|
|
if token_mappings:
|
|
dictionary.mappings.tokens.update(token_mappings)
|
|
if component_mappings:
|
|
dictionary.mappings.components.update(component_mappings)
|
|
if custom_props:
|
|
dictionary.custom_props.update(custom_props)
|
|
if notes:
|
|
dictionary.notes.extend(notes)
|
|
|
|
dictionary.updated_at = datetime.utcnow()
|
|
|
|
# Write back
|
|
await self._write_file(file_path, dictionary)
|
|
|
|
return dictionary
|
|
|
|
async def add_mapping(
|
|
self,
|
|
source: Union[str, TranslationSource],
|
|
source_token: str,
|
|
dss_token: str
|
|
) -> None:
|
|
"""
|
|
Add a single token mapping to a dictionary.
|
|
|
|
Args:
|
|
source: Source type
|
|
source_token: Source token name
|
|
dss_token: DSS canonical path
|
|
"""
|
|
await self.update(
|
|
source=source,
|
|
token_mappings={source_token: dss_token}
|
|
)
|
|
|
|
async def add_custom_prop(
|
|
self,
|
|
source: Union[str, TranslationSource],
|
|
prop_name: str,
|
|
prop_value: Any
|
|
) -> None:
|
|
"""
|
|
Add a custom prop to a dictionary.
|
|
|
|
Args:
|
|
source: Source type
|
|
prop_name: Property name (must use DSS namespace)
|
|
prop_value: Property value
|
|
"""
|
|
# Validate namespace
|
|
if '.' not in prop_name:
|
|
raise ValueError(
|
|
f"Custom prop must use dot-notation namespace: {prop_name}"
|
|
)
|
|
|
|
await self.update(
|
|
source=source,
|
|
custom_props={prop_name: prop_value}
|
|
)
|
|
|
|
async def remove_mapping(
|
|
self,
|
|
source: Union[str, TranslationSource],
|
|
source_token: str
|
|
) -> None:
|
|
"""
|
|
Remove a token mapping from a dictionary.
|
|
|
|
Args:
|
|
source: Source type
|
|
source_token: Source token to remove
|
|
"""
|
|
if isinstance(source, str):
|
|
source = TranslationSource(source)
|
|
|
|
file_path = self.translations_dir / f"{source.value}.json"
|
|
if not file_path.exists():
|
|
return
|
|
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
|
|
dictionary = TranslationDictionary(**data)
|
|
|
|
if source_token in dictionary.mappings.tokens:
|
|
del dictionary.mappings.tokens[source_token]
|
|
dictionary.updated_at = datetime.utcnow()
|
|
await self._write_file(file_path, dictionary)
|
|
|
|
async def mark_unmapped(
|
|
self,
|
|
source: Union[str, TranslationSource],
|
|
unmapped_tokens: List[str]
|
|
) -> None:
|
|
"""
|
|
Add tokens to unmapped list.
|
|
|
|
Args:
|
|
source: Source type
|
|
unmapped_tokens: List of tokens that couldn't be mapped
|
|
"""
|
|
if isinstance(source, str):
|
|
source = TranslationSource(source)
|
|
|
|
file_path = self.translations_dir / f"{source.value}.json"
|
|
if not file_path.exists():
|
|
return
|
|
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
|
|
dictionary = TranslationDictionary(**data)
|
|
|
|
# Add unique unmapped tokens
|
|
existing = set(dictionary.unmapped)
|
|
for token in unmapped_tokens:
|
|
if token not in existing:
|
|
dictionary.unmapped.append(token)
|
|
|
|
dictionary.updated_at = datetime.utcnow()
|
|
await self._write_file(file_path, dictionary)
|
|
|
|
async def _write_file(
|
|
self,
|
|
file_path: Path,
|
|
dictionary: TranslationDictionary
|
|
) -> None:
|
|
"""Write dictionary to JSON file."""
|
|
data = dictionary.model_dump(by_alias=True, mode='json')
|
|
|
|
# Convert datetime to ISO format
|
|
data['created_at'] = dictionary.created_at.isoformat()
|
|
data['updated_at'] = dictionary.updated_at.isoformat()
|
|
|
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
|
|
def delete(
|
|
self,
|
|
source: Union[str, TranslationSource]
|
|
) -> bool:
|
|
"""
|
|
Delete a translation dictionary file.
|
|
|
|
Args:
|
|
source: Source type
|
|
|
|
Returns:
|
|
True if deleted, False if not found
|
|
"""
|
|
if isinstance(source, str):
|
|
source = TranslationSource(source)
|
|
|
|
file_path = self.translations_dir / f"{source.value}.json"
|
|
if file_path.exists():
|
|
file_path.unlink()
|
|
return True
|
|
return False
|
|
```
|
|
|
|
### 4.7 `__init__.py` - Module Exports
|
|
|
|
```python
|
|
"""
|
|
DSS Translation Dictionary Module
|
|
|
|
Provides translation between external design token formats and DSS canonical structure.
|
|
"""
|
|
|
|
from .models import (
|
|
TranslationSource,
|
|
MappingType,
|
|
TokenMapping,
|
|
ComponentMapping,
|
|
PatternMapping,
|
|
CustomProp,
|
|
TranslationMappings,
|
|
TranslationDictionary,
|
|
TranslationRegistry,
|
|
ResolvedToken,
|
|
ResolvedTheme,
|
|
)
|
|
|
|
from .loader import TranslationDictionaryLoader
|
|
from .resolver import TokenResolver
|
|
from .merger import ThemeMerger
|
|
from .validator import TranslationValidator, ValidationResult, ValidationError
|
|
from .writer import TranslationDictionaryWriter
|
|
from .canonical import (
|
|
DSS_CANONICAL_TOKENS,
|
|
DSS_CANONICAL_COMPONENTS,
|
|
DSS_TOKEN_ALIASES,
|
|
DSS_COMPONENT_VARIANTS,
|
|
is_valid_dss_token,
|
|
resolve_alias,
|
|
get_canonical_token_categories,
|
|
)
|
|
|
|
__all__ = [
|
|
# Models
|
|
"TranslationSource",
|
|
"MappingType",
|
|
"TokenMapping",
|
|
"ComponentMapping",
|
|
"PatternMapping",
|
|
"CustomProp",
|
|
"TranslationMappings",
|
|
"TranslationDictionary",
|
|
"TranslationRegistry",
|
|
"ResolvedToken",
|
|
"ResolvedTheme",
|
|
|
|
# Loader
|
|
"TranslationDictionaryLoader",
|
|
|
|
# Resolver
|
|
"TokenResolver",
|
|
|
|
# Merger
|
|
"ThemeMerger",
|
|
|
|
# Validator
|
|
"TranslationValidator",
|
|
"ValidationResult",
|
|
"ValidationError",
|
|
|
|
# Writer
|
|
"TranslationDictionaryWriter",
|
|
|
|
# Canonical Definitions
|
|
"DSS_CANONICAL_TOKENS",
|
|
"DSS_CANONICAL_COMPONENTS",
|
|
"DSS_TOKEN_ALIASES",
|
|
"DSS_COMPONENT_VARIANTS",
|
|
"is_valid_dss_token",
|
|
"resolve_alias",
|
|
"get_canonical_token_categories",
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Implementation Phases
|
|
|
|
### Phase 1: Foundation (Days 1-2)
|
|
|
|
```
|
|
Day 1:
|
|
+-- models.py # Complete data models
|
|
+-- canonical.py # DSS canonical definitions
|
|
+-- __init__.py # Module exports
|
|
|
|
Day 2:
|
|
+-- loader.py # Basic dictionary loading
|
|
+-- validator.py # Schema validation only
|
|
+-- schemas/
|
|
+-- translation-v1.schema.json
|
|
```
|
|
|
|
**Deliverables:**
|
|
- All Pydantic models defined
|
|
- Basic loading from .dss/translations/
|
|
- Schema validation working
|
|
- Unit tests for models
|
|
|
|
### Phase 2: Core Functionality (Days 3-4)
|
|
|
|
```
|
|
Day 3:
|
|
+-- resolver.py # Token resolution
|
|
+-- utils.py # Utility functions
|
|
|
|
Day 4:
|
|
+-- merger.py # Theme merging
|
|
+-- writer.py # Dictionary writing
|
|
```
|
|
|
|
**Deliverables:**
|
|
- Bidirectional token resolution
|
|
- Theme + custom props merging
|
|
- Create/update dictionary files
|
|
- Integration tests
|
|
|
|
### Phase 3: Presets & Polish (Day 5)
|
|
|
|
```
|
|
Day 5:
|
|
+-- presets/
|
|
+-- heroui.json # HeroUI translations
|
|
+-- shadcn.json # shadcn translations
|
|
+-- tailwind.json # Tailwind translations
|
|
+-- Enhanced validation
|
|
+-- Full test coverage
|
|
```
|
|
|
|
**Deliverables:**
|
|
- Pre-built translation dictionaries
|
|
- Semantic validation (not just schema)
|
|
- 90%+ test coverage
|
|
- Documentation
|
|
|
|
---
|
|
|
|
## 6. Testing Strategy
|
|
|
|
### 6.1 Test Structure
|
|
|
|
```
|
|
tests/unit/translations/
|
|
|
|
|
+-- test_models.py # Model validation
|
|
+-- test_loader.py # Dictionary loading
|
|
+-- test_resolver.py # Token resolution
|
|
+-- test_merger.py # Theme merging
|
|
+-- test_validator.py # Validation logic
|
|
+-- test_writer.py # File writing
|
|
+-- test_canonical.py # Canonical definitions
|
|
|
|
tests/integration/translations/
|
|
|
|
|
+-- test_full_workflow.py # End-to-end workflows
|
|
+-- test_mcp_integration.py # MCP tool integration
|
|
|
|
tests/fixtures/translations/
|
|
|
|
|
+-- valid_dictionary.json # Valid test dictionaries
|
|
+-- invalid_schema.json # Invalid schema tests
|
|
+-- conflict_test.json # Conflict detection tests
|
|
```
|
|
|
|
### 6.2 Key Test Cases
|
|
|
|
```python
|
|
# tests/unit/translations/test_resolver.py
|
|
|
|
import pytest
|
|
from dss.translations import (
|
|
TranslationRegistry,
|
|
TranslationDictionary,
|
|
TranslationSource,
|
|
TokenResolver,
|
|
)
|
|
|
|
|
|
class TestTokenResolver:
|
|
"""Test suite for TokenResolver."""
|
|
|
|
@pytest.fixture
|
|
def sample_registry(self):
|
|
"""Create registry with sample mappings."""
|
|
dictionary = TranslationDictionary(
|
|
project="test",
|
|
source=TranslationSource.CSS,
|
|
mappings={
|
|
"tokens": {
|
|
"--brand-blue": "color.primary.500",
|
|
"--brand-dark": "color.primary.700",
|
|
"--text-main": "color.neutral.900",
|
|
}
|
|
}
|
|
)
|
|
registry = TranslationRegistry()
|
|
registry.dictionaries["css"] = dictionary
|
|
registry.combined_token_map = dictionary.mappings.tokens
|
|
return registry
|
|
|
|
def test_forward_resolution(self, sample_registry):
|
|
"""Test source -> DSS resolution."""
|
|
resolver = TokenResolver(sample_registry)
|
|
|
|
result = resolver.resolve_to_dss("--brand-blue")
|
|
assert result == "color.primary.500"
|
|
|
|
def test_reverse_resolution(self, sample_registry):
|
|
"""Test DSS -> source resolution."""
|
|
resolver = TokenResolver(sample_registry)
|
|
|
|
result = resolver.resolve_to_source("color.primary.500", "css")
|
|
assert result == "--brand-blue"
|
|
|
|
def test_unknown_token_returns_none(self, sample_registry):
|
|
"""Test that unknown tokens return None."""
|
|
resolver = TokenResolver(sample_registry)
|
|
|
|
result = resolver.resolve_to_dss("--unknown-token")
|
|
assert result is None
|
|
|
|
def test_normalize_var_reference(self, sample_registry):
|
|
"""Test normalization of var() references."""
|
|
resolver = TokenResolver(sample_registry)
|
|
|
|
# Should normalize var(--brand-blue) to --brand-blue
|
|
result = resolver.resolve_to_dss("var(--brand-blue)")
|
|
assert result == "color.primary.500"
|
|
```
|
|
|
|
### 6.3 Integration Test Example
|
|
|
|
```python
|
|
# tests/integration/translations/test_full_workflow.py
|
|
|
|
import pytest
|
|
import tempfile
|
|
from pathlib import Path
|
|
from dss.translations import (
|
|
TranslationDictionaryLoader,
|
|
TranslationDictionaryWriter,
|
|
ThemeMerger,
|
|
TokenResolver,
|
|
TranslationSource,
|
|
)
|
|
|
|
|
|
class TestFullWorkflow:
|
|
"""Test complete translation workflow."""
|
|
|
|
@pytest.fixture
|
|
def temp_project(self):
|
|
"""Create temporary project directory."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
project_path = Path(tmpdir)
|
|
translations_dir = project_path / ".dss" / "translations"
|
|
translations_dir.mkdir(parents=True)
|
|
yield project_path
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_load_resolve_workflow(self, temp_project):
|
|
"""Test: Create dictionary -> Load -> Resolve -> Merge."""
|
|
|
|
# 1. Create dictionary
|
|
writer = TranslationDictionaryWriter(temp_project)
|
|
await writer.create(
|
|
source=TranslationSource.CSS,
|
|
project="test-project",
|
|
token_mappings={
|
|
"--brand-blue": "color.primary.500",
|
|
"--space-sm": "spacing.sm",
|
|
},
|
|
custom_props={
|
|
"color.brand.test.accent": "#FF5733"
|
|
}
|
|
)
|
|
|
|
# 2. Load dictionary
|
|
loader = TranslationDictionaryLoader(temp_project)
|
|
registry = await loader.load_all()
|
|
|
|
assert len(registry.dictionaries) == 1
|
|
assert "css" in registry.dictionaries
|
|
|
|
# 3. Resolve tokens
|
|
resolver = TokenResolver(registry)
|
|
|
|
dss_path = resolver.resolve_to_dss("--brand-blue")
|
|
assert dss_path == "color.primary.500"
|
|
|
|
source_token = resolver.resolve_to_source("color.primary.500", "css")
|
|
assert source_token == "--brand-blue"
|
|
|
|
# 4. Merge with base theme
|
|
merger = ThemeMerger(registry)
|
|
resolved_theme = await merger.merge(base_theme="light", project_name="test")
|
|
|
|
assert resolved_theme.name == "test"
|
|
assert "color.brand.test.accent" in resolved_theme.custom_props
|
|
assert len(resolved_theme.translations_applied) == 1
|
|
```
|
|
|
|
---
|
|
|
|
## 7. MCP Integration Plan
|
|
|
|
### 7.1 New MCP Tools
|
|
|
|
After the Python module is complete, add these MCP tools:
|
|
|
|
```python
|
|
# tools/dss_mcp/integrations/translations.py
|
|
|
|
from typing import Dict, Any, List, Optional
|
|
from dss.translations import (
|
|
TranslationDictionaryLoader,
|
|
TranslationDictionaryWriter,
|
|
ThemeMerger,
|
|
TokenResolver,
|
|
TranslationValidator,
|
|
TranslationSource,
|
|
)
|
|
|
|
|
|
class TranslationIntegration:
|
|
"""MCP integration for translation dictionaries."""
|
|
|
|
async def translation_load_all(
|
|
self,
|
|
project_path: str
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Load all translation dictionaries for a project.
|
|
|
|
MCP Tool: translation_load_all
|
|
"""
|
|
loader = TranslationDictionaryLoader(project_path)
|
|
registry = await loader.load_all()
|
|
|
|
return {
|
|
"dictionaries": [
|
|
dict_obj.model_dump(mode='json')
|
|
for dict_obj in registry.dictionaries.values()
|
|
],
|
|
"total_token_mappings": len(registry.combined_token_map),
|
|
"total_custom_props": len(registry.all_custom_props),
|
|
"conflicts": registry.conflicts,
|
|
}
|
|
|
|
async def translation_resolve_token(
|
|
self,
|
|
project_path: str,
|
|
source_token: str,
|
|
source_type: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Resolve a source token to DSS canonical.
|
|
|
|
MCP Tool: translation_resolve_token
|
|
"""
|
|
loader = TranslationDictionaryLoader(project_path)
|
|
registry = await loader.load_all()
|
|
resolver = TokenResolver(registry)
|
|
|
|
dss_path = resolver.resolve_to_dss(source_token, source_type)
|
|
|
|
return {
|
|
"source_token": source_token,
|
|
"dss_path": dss_path,
|
|
"found": dss_path is not None,
|
|
}
|
|
|
|
async def translation_create_dictionary(
|
|
self,
|
|
project_path: str,
|
|
source: str,
|
|
project_name: str,
|
|
token_mappings: Optional[Dict[str, str]] = None,
|
|
custom_props: Optional[Dict[str, Any]] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create a new translation dictionary.
|
|
|
|
MCP Tool: translation_create
|
|
"""
|
|
writer = TranslationDictionaryWriter(project_path)
|
|
dictionary = await writer.create(
|
|
source=TranslationSource(source),
|
|
project=project_name,
|
|
token_mappings=token_mappings,
|
|
custom_props=custom_props,
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"dictionary": dictionary.model_dump(mode='json'),
|
|
"file_path": str(writer.translations_dir / f"{source}.json"),
|
|
}
|
|
|
|
async def translation_add_mapping(
|
|
self,
|
|
project_path: str,
|
|
source: str,
|
|
source_token: str,
|
|
dss_token: str
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Add a token mapping to an existing dictionary.
|
|
|
|
MCP Tool: translation_add_mapping
|
|
"""
|
|
writer = TranslationDictionaryWriter(project_path)
|
|
await writer.add_mapping(
|
|
source=TranslationSource(source),
|
|
source_token=source_token,
|
|
dss_token=dss_token,
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"mapping": {source_token: dss_token},
|
|
}
|
|
|
|
async def translation_validate(
|
|
self,
|
|
project_path: str,
|
|
source: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Validate translation dictionaries.
|
|
|
|
MCP Tool: translation_validate
|
|
"""
|
|
loader = TranslationDictionaryLoader(project_path, validate=False)
|
|
validator = TranslationValidator()
|
|
|
|
results = []
|
|
|
|
if source:
|
|
dictionary = await loader.load_dictionary(source)
|
|
if dictionary:
|
|
result = validator.validate_dictionary(dictionary.model_dump())
|
|
results.append({
|
|
"source": source,
|
|
"is_valid": result.is_valid,
|
|
"errors": [str(e) for e in result.errors],
|
|
"warnings": [str(w) for w in result.warnings],
|
|
})
|
|
else:
|
|
for dict_file in loader.translations_dir.glob("*.json"):
|
|
dictionary = await loader.load_dictionary_file(dict_file)
|
|
result = validator.validate_dictionary(dictionary.model_dump())
|
|
results.append({
|
|
"source": dict_file.stem,
|
|
"is_valid": result.is_valid,
|
|
"errors": [str(e) for e in result.errors],
|
|
"warnings": [str(w) for w in result.warnings],
|
|
})
|
|
|
|
all_valid = all(r["is_valid"] for r in results)
|
|
|
|
return {
|
|
"all_valid": all_valid,
|
|
"results": results,
|
|
}
|
|
|
|
async def translation_merge_theme(
|
|
self,
|
|
project_path: str,
|
|
base_theme: str = "light",
|
|
project_name: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Merge base theme with translations and custom props.
|
|
|
|
MCP Tool: translation_merge_theme
|
|
"""
|
|
loader = TranslationDictionaryLoader(project_path)
|
|
registry = await loader.load_all()
|
|
merger = ThemeMerger(registry)
|
|
|
|
resolved = await merger.merge(
|
|
base_theme=base_theme,
|
|
project_name=project_name,
|
|
)
|
|
|
|
return {
|
|
"name": resolved.name,
|
|
"base_theme": resolved.base_theme,
|
|
"token_count": len(resolved.tokens),
|
|
"custom_prop_count": len(resolved.custom_props),
|
|
"translations_applied": resolved.translations_applied,
|
|
"tokens": {
|
|
path: token.model_dump(mode='json')
|
|
for path, token in resolved.tokens.items()
|
|
},
|
|
"custom_props": {
|
|
path: token.model_dump(mode='json')
|
|
for path, token in resolved.custom_props.items()
|
|
},
|
|
}
|
|
```
|
|
|
|
### 7.2 MCP Tool Manifest Update
|
|
|
|
```python
|
|
# Add to tools/dss_mcp/handler.py
|
|
|
|
TRANSLATION_TOOLS = [
|
|
{
|
|
"name": "translation_load_all",
|
|
"description": "Load all translation dictionaries for a project",
|
|
"parameters": {
|
|
"project_path": {"type": "string", "required": True}
|
|
}
|
|
},
|
|
{
|
|
"name": "translation_resolve_token",
|
|
"description": "Resolve a source token to DSS canonical path",
|
|
"parameters": {
|
|
"project_path": {"type": "string", "required": True},
|
|
"source_token": {"type": "string", "required": True},
|
|
"source_type": {"type": "string", "required": False}
|
|
}
|
|
},
|
|
{
|
|
"name": "translation_create",
|
|
"description": "Create a new translation dictionary",
|
|
"parameters": {
|
|
"project_path": {"type": "string", "required": True},
|
|
"source": {"type": "string", "required": True},
|
|
"project_name": {"type": "string", "required": True},
|
|
"token_mappings": {"type": "object", "required": False},
|
|
"custom_props": {"type": "object", "required": False}
|
|
}
|
|
},
|
|
{
|
|
"name": "translation_add_mapping",
|
|
"description": "Add a token mapping to a dictionary",
|
|
"parameters": {
|
|
"project_path": {"type": "string", "required": True},
|
|
"source": {"type": "string", "required": True},
|
|
"source_token": {"type": "string", "required": True},
|
|
"dss_token": {"type": "string", "required": True}
|
|
}
|
|
},
|
|
{
|
|
"name": "translation_validate",
|
|
"description": "Validate translation dictionaries",
|
|
"parameters": {
|
|
"project_path": {"type": "string", "required": True},
|
|
"source": {"type": "string", "required": False}
|
|
}
|
|
},
|
|
{
|
|
"name": "translation_merge_theme",
|
|
"description": "Merge base theme with translations and custom props",
|
|
"parameters": {
|
|
"project_path": {"type": "string", "required": True},
|
|
"base_theme": {"type": "string", "required": False, "default": "light"},
|
|
"project_name": {"type": "string", "required": False}
|
|
}
|
|
}
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Example Usage
|
|
|
|
### 8.1 Example Translation Dictionary - HeroUI
|
|
|
|
```json
|
|
{
|
|
"$schema": "dss-translation-v1",
|
|
"project": "heroui-migration",
|
|
"source": "heroui",
|
|
"version": "1.0.0",
|
|
"mappings": {
|
|
"tokens": {
|
|
"--heroui-primary-50": "color.primary.50",
|
|
"--heroui-primary-100": "color.primary.100",
|
|
"--heroui-primary-200": "color.primary.200",
|
|
"--heroui-primary-300": "color.primary.300",
|
|
"--heroui-primary-400": "color.primary.400",
|
|
"--heroui-primary-500": "color.primary.500",
|
|
"--heroui-primary-600": "color.primary.600",
|
|
"--heroui-primary-700": "color.primary.700",
|
|
"--heroui-primary-800": "color.primary.800",
|
|
"--heroui-primary-900": "color.primary.900",
|
|
"--heroui-content1": "color.neutral.50",
|
|
"--heroui-content2": "color.neutral.100",
|
|
"--heroui-content3": "color.neutral.200",
|
|
"--heroui-content4": "color.neutral.300",
|
|
"--heroui-radius-small": "border.radius.sm",
|
|
"--heroui-radius-medium": "border.radius.md",
|
|
"--heroui-radius-large": "border.radius.lg",
|
|
"--heroui-shadow-small": "shadow.sm",
|
|
"--heroui-shadow-medium": "shadow.md",
|
|
"--heroui-shadow-large": "shadow.lg"
|
|
},
|
|
"components": {
|
|
"Button": "Button",
|
|
"Card": "Card",
|
|
"Input": "Input",
|
|
"Modal": "Modal",
|
|
"Dropdown": "Select"
|
|
}
|
|
},
|
|
"custom_props": {},
|
|
"unmapped": [],
|
|
"notes": [
|
|
"HeroUI uses numeric scales - direct 1:1 mapping",
|
|
"Content layers map to neutral scale"
|
|
]
|
|
}
|
|
```
|
|
|
|
### 8.2 Example Translation Dictionary - shadcn
|
|
|
|
```json
|
|
{
|
|
"$schema": "dss-translation-v1",
|
|
"project": "shadcn-migration",
|
|
"source": "shadcn",
|
|
"version": "1.0.0",
|
|
"mappings": {
|
|
"tokens": {
|
|
"--background": "color.background",
|
|
"--foreground": "color.foreground",
|
|
"--primary": "color.primary.500",
|
|
"--primary-foreground": "color.primary.50",
|
|
"--secondary": "color.secondary.500",
|
|
"--secondary-foreground": "color.secondary.50",
|
|
"--muted": "color.neutral.200",
|
|
"--muted-foreground": "color.neutral.600",
|
|
"--accent": "color.accent.500",
|
|
"--accent-foreground": "color.accent.50",
|
|
"--destructive": "color.danger.500",
|
|
"--card": "color.neutral.50",
|
|
"--card-foreground": "color.foreground",
|
|
"--popover": "color.neutral.50",
|
|
"--popover-foreground": "color.foreground",
|
|
"--border": "color.border",
|
|
"--input": "color.neutral.200",
|
|
"--ring": "color.ring",
|
|
"--radius": "border.radius.md"
|
|
},
|
|
"components": {
|
|
"Button": "Button",
|
|
"Card": "Card",
|
|
"Input": "Input",
|
|
"Dialog": "Modal",
|
|
"Popover": "Popover",
|
|
"Select": "Select"
|
|
}
|
|
},
|
|
"custom_props": {
|
|
"color.brand.shadcn.chart.1": "hsl(12 76% 61%)",
|
|
"color.brand.shadcn.chart.2": "hsl(173 58% 39%)",
|
|
"color.brand.shadcn.chart.3": "hsl(197 37% 24%)",
|
|
"color.brand.shadcn.sidebar.primary": "var(--sidebar-primary)",
|
|
"color.brand.shadcn.sidebar.accent": "var(--sidebar-accent)"
|
|
},
|
|
"unmapped": [
|
|
"--chart-1",
|
|
"--chart-2",
|
|
"--chart-3",
|
|
"--chart-4",
|
|
"--chart-5"
|
|
],
|
|
"notes": [
|
|
"shadcn is HEADLESS - no numeric color scales",
|
|
"Semantic names expand to 500 (default) DSS values",
|
|
"Chart colors are shadcn-specific, isolated in custom_props"
|
|
]
|
|
}
|
|
```
|
|
|
|
### 8.3 Example Python Usage
|
|
|
|
```python
|
|
# Example: Complete workflow
|
|
|
|
import asyncio
|
|
from dss.translations import (
|
|
TranslationDictionaryLoader,
|
|
TranslationDictionaryWriter,
|
|
ThemeMerger,
|
|
TokenResolver,
|
|
TranslationSource,
|
|
)
|
|
|
|
|
|
async def main():
|
|
project_path = "/path/to/my-project"
|
|
|
|
# 1. Create translation dictionaries for project
|
|
writer = TranslationDictionaryWriter(project_path)
|
|
|
|
# Create CSS legacy mappings
|
|
await writer.create(
|
|
source=TranslationSource.CSS,
|
|
project="my-project",
|
|
token_mappings={
|
|
"--brand-primary": "color.primary.500",
|
|
"--brand-secondary": "color.secondary.500",
|
|
"--spacing-unit": "spacing.base",
|
|
"--card-radius": "border.radius.md",
|
|
},
|
|
custom_props={
|
|
"color.brand.myproject.gradient.start": "#FF6B6B",
|
|
"color.brand.myproject.gradient.end": "#4ECDC4",
|
|
},
|
|
notes=["Migrated from legacy CSS variables"]
|
|
)
|
|
|
|
# 2. Load all dictionaries
|
|
loader = TranslationDictionaryLoader(project_path)
|
|
registry = await loader.load_all()
|
|
|
|
print(f"Loaded {len(registry.dictionaries)} dictionaries")
|
|
print(f"Total mappings: {len(registry.combined_token_map)}")
|
|
print(f"Custom props: {len(registry.all_custom_props)}")
|
|
|
|
# 3. Resolve tokens
|
|
resolver = TokenResolver(registry)
|
|
|
|
# Forward: source -> DSS
|
|
dss_path = resolver.resolve_to_dss("--brand-primary")
|
|
print(f"--brand-primary -> {dss_path}")
|
|
|
|
# Reverse: DSS -> source
|
|
source_token = resolver.resolve_to_source("color.primary.500", "css")
|
|
print(f"color.primary.500 -> {source_token}")
|
|
|
|
# 4. Merge with base theme
|
|
merger = ThemeMerger(registry)
|
|
resolved = await merger.merge(
|
|
base_theme="light",
|
|
project_name="my-project"
|
|
)
|
|
|
|
print(f"\nResolved theme: {resolved.name}")
|
|
print(f" Base: {resolved.base_theme}")
|
|
print(f" Tokens: {len(resolved.tokens)}")
|
|
print(f" Custom props: {len(resolved.custom_props)}")
|
|
|
|
# 5. Export as Theme object (for Storybook integration)
|
|
theme = merger.export_as_theme(resolved)
|
|
print(f"\nExported Theme: {theme.name}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Dependencies
|
|
|
|
### 9.1 Python Dependencies
|
|
|
|
```
|
|
# No new dependencies required!
|
|
# Uses existing DSS dependencies:
|
|
|
|
pydantic>=2.0.0 # Already in requirements.txt
|
|
pydantic-settings>=2.0.0 # Already in requirements.txt
|
|
```
|
|
|
|
### 9.2 Integration with Existing Modules
|
|
|
|
| Module | Integration Type | Description |
|
|
|--------|------------------|-------------|
|
|
| `dss.models.theme` | Import | Use Theme, DesignToken models |
|
|
| `dss.themes` | Import | Use default light/dark themes |
|
|
| `dss.ingest` | Complement | Translations work with ingested tokens |
|
|
| `dss.validators` | Pattern | Follow validation patterns |
|
|
| `dss.storybook` | Consumer | Storybook uses merged themes |
|
|
|
|
---
|
|
|
|
## 10. Risks & Mitigations
|
|
|
|
| Risk | Impact | Likelihood | Mitigation |
|
|
|------|--------|------------|------------|
|
|
| Schema changes break existing dictionaries | High | Medium | Version schema, provide migrations |
|
|
| Performance with large dictionaries | Medium | Low | Implement caching, lazy loading |
|
|
| Circular token references | High | Low | Detect cycles during resolution |
|
|
| Conflict resolution ambiguity | Medium | Medium | Clear precedence rules, manual override |
|
|
| MCP tool complexity | Medium | Medium | Simple API, comprehensive docs |
|
|
|
|
---
|
|
|
|
## 11. Success Criteria
|
|
|
|
### Phase 1 (Foundation)
|
|
- [ ] All Pydantic models pass validation tests
|
|
- [ ] Dictionary loader can read .dss/translations/*.json
|
|
- [ ] Schema validation catches invalid dictionaries
|
|
- [ ] 80%+ unit test coverage
|
|
|
|
### Phase 2 (Core)
|
|
- [ ] Forward resolution works (source -> DSS)
|
|
- [ ] Reverse resolution works (DSS -> source)
|
|
- [ ] Theme merger produces valid themes
|
|
- [ ] Writer can create/update dictionaries
|
|
- [ ] Integration tests pass
|
|
|
|
### Phase 3 (Polish)
|
|
- [ ] Pre-built dictionaries for HeroUI, shadcn, Tailwind
|
|
- [ ] Semantic validation (not just schema)
|
|
- [ ] 90%+ test coverage
|
|
- [ ] Documentation complete
|
|
- [ ] MCP integration working
|
|
|
|
---
|
|
|
|
## 12. Next Steps
|
|
|
|
1. **Review this plan** with core team
|
|
2. **Approve architecture** and data models
|
|
3. **Begin Phase 1** implementation
|
|
4. **Create GitHub issues** for tracking
|
|
5. **Set up CI/CD** for test automation
|
|
|
|
---
|
|
|
|
## Appendix A: DSS Config Schema
|
|
|
|
```json
|
|
{
|
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
"$id": "https://dss.dev/schemas/config.schema.json",
|
|
"title": "DSS Project Configuration",
|
|
"description": "Project-level DSS configuration",
|
|
"type": "object",
|
|
"required": ["project", "version"],
|
|
"properties": {
|
|
"project": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Project identifier"
|
|
},
|
|
"version": {
|
|
"type": "string",
|
|
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
|
"description": "Configuration version"
|
|
},
|
|
"base_theme": {
|
|
"type": "string",
|
|
"enum": ["light", "dark"],
|
|
"default": "light",
|
|
"description": "Base theme to use"
|
|
},
|
|
"translations": {
|
|
"type": "object",
|
|
"properties": {
|
|
"sources": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string",
|
|
"enum": ["figma", "css", "scss", "heroui", "shadcn", "tailwind", "json"]
|
|
},
|
|
"description": "Active translation sources"
|
|
},
|
|
"auto_map": {
|
|
"type": "boolean",
|
|
"default": true,
|
|
"description": "Enable automatic token mapping"
|
|
}
|
|
}
|
|
},
|
|
"output": {
|
|
"type": "object",
|
|
"properties": {
|
|
"formats": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string",
|
|
"enum": ["css", "scss", "json", "typescript"]
|
|
},
|
|
"description": "Output formats to generate"
|
|
},
|
|
"dir": {
|
|
"type": "string",
|
|
"default": "dist/tokens",
|
|
"description": "Output directory"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
**Document End**
|
|
|
|
*This implementation plan provides a complete blueprint for the Translation Dictionary System. The architecture follows existing DSS patterns, uses Pydantic for data validation, and integrates seamlessly with the MCP plugin infrastructure.*
|