mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Default English resource i18n fallback (#1270)
This commit is contained in:
parent
12ac2e988e
commit
be77dc0394
8 changed files with 672 additions and 960 deletions
|
|
@ -323,251 +323,6 @@ export const FR_DESIGN_SYSTEM_CATEGORIES: Record<string, string> = {
|
|||
Uncategorized: 'Non catégorisé',
|
||||
};
|
||||
|
||||
export const FR_SKILL_IDS_WITH_EN_FALLBACK = [
|
||||
'clinical-case-report',
|
||||
'dcf-valuation',
|
||||
'editorial-burgundy-principles-template',
|
||||
'flowai-live-dashboard-template',
|
||||
'html-ppt-taste-brutalist',
|
||||
'html-ppt-taste-editorial',
|
||||
// Vendored upstream English-language Zara templates (zarazhangrui/beautiful-html-templates).
|
||||
// Localized copy is not maintained; fall back to the upstream English description.
|
||||
'html-ppt-zhangzara-8-bit-orbit',
|
||||
'html-ppt-zhangzara-biennale-yellow',
|
||||
'html-ppt-zhangzara-block-frame',
|
||||
'html-ppt-zhangzara-blue-professional',
|
||||
'html-ppt-zhangzara-bold-poster',
|
||||
'html-ppt-zhangzara-broadside',
|
||||
'html-ppt-zhangzara-capsule',
|
||||
'html-ppt-zhangzara-cartesian',
|
||||
'html-ppt-zhangzara-cobalt-grid',
|
||||
'html-ppt-zhangzara-coral',
|
||||
'html-ppt-zhangzara-creative-mode',
|
||||
'html-ppt-zhangzara-daisy-days',
|
||||
'html-ppt-zhangzara-editorial-tri-tone',
|
||||
'html-ppt-zhangzara-grove',
|
||||
'html-ppt-zhangzara-long-table',
|
||||
'html-ppt-zhangzara-mat',
|
||||
'html-ppt-zhangzara-monochrome',
|
||||
'html-ppt-zhangzara-neo-grid-bold',
|
||||
'html-ppt-zhangzara-peoples-platform',
|
||||
'html-ppt-zhangzara-pin-and-paper',
|
||||
'html-ppt-zhangzara-pink-script',
|
||||
'html-ppt-zhangzara-playful',
|
||||
'html-ppt-zhangzara-raw-grid',
|
||||
'html-ppt-zhangzara-retro-windows',
|
||||
'html-ppt-zhangzara-retro-zine',
|
||||
'html-ppt-zhangzara-sakura-chroma',
|
||||
'html-ppt-zhangzara-scatterbrain',
|
||||
'html-ppt-zhangzara-signal',
|
||||
'html-ppt-zhangzara-soft-editorial',
|
||||
'html-ppt-zhangzara-stencil-tablet',
|
||||
'html-ppt-zhangzara-studio',
|
||||
'html-ppt-zhangzara-vellum',
|
||||
// IB pitch-book skill (#888): English-only skill copy for now.
|
||||
'ib-pitch-book',
|
||||
'last30days',
|
||||
'live-dashboard',
|
||||
'login-flow',
|
||||
'orbit-general',
|
||||
'orbit-github',
|
||||
'orbit-gmail',
|
||||
'orbit-linear',
|
||||
'orbit-notion',
|
||||
'release-notes-one-pager',
|
||||
// TODO: add localized copy for social-media-dashboard (introduced in #678).
|
||||
// Fallback for now so the localized-content coverage test passes.
|
||||
'social-media-dashboard',
|
||||
'social-media-matrix-tracker-template',
|
||||
'8-bit-orbit-video-template',
|
||||
'digits-fintech-swiss-template',
|
||||
'field-notes-editorial-template',
|
||||
'html-ppt-retro-quarterly-review',
|
||||
'swiss-user-research-video-template',
|
||||
'web-prototype-taste-brutalist',
|
||||
'web-prototype-taste-editorial',
|
||||
'web-prototype-taste-soft',
|
||||
'waitlist-page',
|
||||
'x-research',
|
||||
'trading-analysis-dashboard-template',
|
||||
'swiss-creative-mode-template',
|
||||
'github-dashboard',
|
||||
'after-hours-editorial-template',
|
||||
// Curated design/creative skill catalogue (PR #955) — lightweight stubs
|
||||
// pointing at upstream awesome-claude-skills / awesome-agent-skills
|
||||
// entries; English-only by design. The localized-content coverage test
|
||||
// treats these as English-fallback so the stubs land in Settings →
|
||||
// Skills without forcing a localization task per locale.
|
||||
'ad-creative',
|
||||
'ai-music-album',
|
||||
'algorithmic-art',
|
||||
'apple-hig',
|
||||
'artifacts-builder',
|
||||
'brainstorming',
|
||||
'brand-guidelines',
|
||||
'canvas-design',
|
||||
'color-expert',
|
||||
'competitive-ads-extractor',
|
||||
'copywriting',
|
||||
'creative-director',
|
||||
'd3-visualization',
|
||||
'design-consultation',
|
||||
'design-md',
|
||||
'design-review',
|
||||
'doc',
|
||||
'docx',
|
||||
'domain-name-brainstormer',
|
||||
'enhance-prompt',
|
||||
'fal-3d',
|
||||
'fal-generate',
|
||||
'fal-image-edit',
|
||||
'fal-kling-o3',
|
||||
'fal-lip-sync',
|
||||
'fal-realtime',
|
||||
'fal-restore',
|
||||
'fal-train',
|
||||
'fal-tryon',
|
||||
'fal-upscale',
|
||||
'fal-video-edit',
|
||||
'fal-vision',
|
||||
'figma-code-connect-components',
|
||||
'figma-create-design-system-rules',
|
||||
'figma-create-new-file',
|
||||
'figma-generate-design',
|
||||
'figma-generate-library',
|
||||
'figma-implement-design',
|
||||
'figma-use',
|
||||
'flutter-animating-apps',
|
||||
'frontend-design',
|
||||
'frontend-dev',
|
||||
'frontend-skill',
|
||||
'frontend-slides',
|
||||
'full-page-screenshot',
|
||||
'gif-sticker-maker',
|
||||
'gsap-core',
|
||||
'gsap-react',
|
||||
'gsap-scrolltrigger',
|
||||
'gsap-timeline',
|
||||
'hand-drawn-diagrams',
|
||||
'image-enhancer',
|
||||
'imagegen',
|
||||
'imagen',
|
||||
'marketing-psychology',
|
||||
'minimax-docx',
|
||||
'minimax-pdf',
|
||||
'nanobanana-ppt',
|
||||
'paywall-upgrade-cro',
|
||||
'pdf',
|
||||
'pixelbin-media',
|
||||
'plan-design-review',
|
||||
'platform-design',
|
||||
'pptx',
|
||||
'pptx-generator',
|
||||
'remotion',
|
||||
'replicate',
|
||||
'screenshot',
|
||||
'screenshots-marketing',
|
||||
'shadcn-ui',
|
||||
'shader-dev',
|
||||
'slack-gif-creator',
|
||||
'slides',
|
||||
'sora',
|
||||
'speech',
|
||||
'stitch-loop',
|
||||
'swiftui-design',
|
||||
'taste-skill',
|
||||
'theme-factory',
|
||||
'threejs',
|
||||
'ui-skills',
|
||||
'ui-ux-pro-max',
|
||||
'venice-audio-music',
|
||||
'venice-audio-speech',
|
||||
'venice-image-edit',
|
||||
'venice-image-generate',
|
||||
'venice-video',
|
||||
'video-downloader',
|
||||
'web-artifacts-builder',
|
||||
'web-design-guidelines',
|
||||
'wpds',
|
||||
'youtube-clipper',
|
||||
] as const;
|
||||
|
||||
export const FR_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [
|
||||
'agentic',
|
||||
'ant',
|
||||
'application',
|
||||
'arc',
|
||||
'artistic',
|
||||
'bento',
|
||||
'bmw-m',
|
||||
'bold',
|
||||
'brutalism',
|
||||
'cafe',
|
||||
'canva',
|
||||
'cisco',
|
||||
'claymorphism',
|
||||
'clean',
|
||||
'colorful',
|
||||
'contemporary',
|
||||
'corporate',
|
||||
'cosmic',
|
||||
'creative',
|
||||
'dashboard',
|
||||
'discord',
|
||||
'dithered',
|
||||
'doodle',
|
||||
'dramatic',
|
||||
'duolingo',
|
||||
'editorial',
|
||||
'elegant',
|
||||
'energetic',
|
||||
'enterprise',
|
||||
'expressive',
|
||||
'fantasy',
|
||||
'flat',
|
||||
'friendly',
|
||||
'futuristic',
|
||||
'github',
|
||||
'glassmorphism',
|
||||
'gradient',
|
||||
'huggingface',
|
||||
'hud',
|
||||
'levels',
|
||||
'lingo',
|
||||
'luxury',
|
||||
'material',
|
||||
'minimal',
|
||||
'mission-control',
|
||||
'modern',
|
||||
'mono',
|
||||
'neobrutalism',
|
||||
'neon',
|
||||
'neumorphism',
|
||||
'openai',
|
||||
'pacman',
|
||||
'paper',
|
||||
'perspective',
|
||||
'premium',
|
||||
'professional',
|
||||
'publication',
|
||||
'refined',
|
||||
'retro',
|
||||
'shadcn',
|
||||
'simple',
|
||||
'skeumorphism',
|
||||
'slack',
|
||||
'sleek',
|
||||
'spacious',
|
||||
'storytelling',
|
||||
'totality-festival',
|
||||
'tetris',
|
||||
'urdu',
|
||||
'vibrant',
|
||||
'vintage',
|
||||
'wechat',
|
||||
'webex',
|
||||
] as const;
|
||||
|
||||
export const FR_PROMPT_TEMPLATE_CATEGORIES: Record<string, string> = {
|
||||
Infographic: 'Infographie',
|
||||
'Anime / Manga': 'Anime / manga',
|
||||
|
|
@ -593,17 +348,6 @@ export const FR_PROMPT_TEMPLATE_CATEGORIES: Record<string, string> = {
|
|||
'VFX / HTML-in-Canvas': 'VFX / HTML-in-Canvas',
|
||||
};
|
||||
|
||||
export const FR_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK = [
|
||||
'notion-team-dashboard-live-artifact',
|
||||
'hyperframes-html-in-canvas-iphone-device',
|
||||
'hyperframes-html-in-canvas-liquid-background',
|
||||
'hyperframes-html-in-canvas-liquid-glass',
|
||||
'hyperframes-html-in-canvas-magnetic',
|
||||
'hyperframes-html-in-canvas-portal-reveal',
|
||||
'hyperframes-html-in-canvas-shatter',
|
||||
'hyperframes-html-in-canvas-text-cursor',
|
||||
] as const;
|
||||
|
||||
export const FR_PROMPT_TEMPLATE_TAGS: Record<string, string> = {
|
||||
'3d': '3D',
|
||||
'3d-render': 'rendu 3D',
|
||||
|
|
|
|||
|
|
@ -323,251 +323,6 @@ export const RU_DESIGN_SYSTEM_CATEGORIES: Record<string, string> = {
|
|||
Uncategorized: 'Без категории',
|
||||
};
|
||||
|
||||
export const RU_SKILL_IDS_WITH_EN_FALLBACK = [
|
||||
'clinical-case-report',
|
||||
'dcf-valuation',
|
||||
'editorial-burgundy-principles-template',
|
||||
'flowai-live-dashboard-template',
|
||||
'html-ppt-taste-brutalist',
|
||||
'html-ppt-taste-editorial',
|
||||
// Vendored upstream English-language Zara templates (zarazhangrui/beautiful-html-templates).
|
||||
// Localized copy is not maintained; fall back to the upstream English description.
|
||||
'html-ppt-zhangzara-8-bit-orbit',
|
||||
'html-ppt-zhangzara-biennale-yellow',
|
||||
'html-ppt-zhangzara-block-frame',
|
||||
'html-ppt-zhangzara-blue-professional',
|
||||
'html-ppt-zhangzara-bold-poster',
|
||||
'html-ppt-zhangzara-broadside',
|
||||
'html-ppt-zhangzara-capsule',
|
||||
'html-ppt-zhangzara-cartesian',
|
||||
'html-ppt-zhangzara-cobalt-grid',
|
||||
'html-ppt-zhangzara-coral',
|
||||
'html-ppt-zhangzara-creative-mode',
|
||||
'html-ppt-zhangzara-daisy-days',
|
||||
'html-ppt-zhangzara-editorial-tri-tone',
|
||||
'html-ppt-zhangzara-grove',
|
||||
'html-ppt-zhangzara-long-table',
|
||||
'html-ppt-zhangzara-mat',
|
||||
'html-ppt-zhangzara-monochrome',
|
||||
'html-ppt-zhangzara-neo-grid-bold',
|
||||
'html-ppt-zhangzara-peoples-platform',
|
||||
'html-ppt-zhangzara-pin-and-paper',
|
||||
'html-ppt-zhangzara-pink-script',
|
||||
'html-ppt-zhangzara-playful',
|
||||
'html-ppt-zhangzara-raw-grid',
|
||||
'html-ppt-zhangzara-retro-windows',
|
||||
'html-ppt-zhangzara-retro-zine',
|
||||
'html-ppt-zhangzara-sakura-chroma',
|
||||
'html-ppt-zhangzara-scatterbrain',
|
||||
'html-ppt-zhangzara-signal',
|
||||
'html-ppt-zhangzara-soft-editorial',
|
||||
'html-ppt-zhangzara-stencil-tablet',
|
||||
'html-ppt-zhangzara-studio',
|
||||
'html-ppt-zhangzara-vellum',
|
||||
// IB pitch-book skill (#888): English-only skill copy for now.
|
||||
'ib-pitch-book',
|
||||
'last30days',
|
||||
'live-dashboard',
|
||||
'login-flow',
|
||||
'orbit-general',
|
||||
'orbit-github',
|
||||
'orbit-gmail',
|
||||
'orbit-linear',
|
||||
'orbit-notion',
|
||||
'release-notes-one-pager',
|
||||
// TODO: add localized copy for social-media-dashboard (introduced in #678).
|
||||
// Fallback for now so the localized-content coverage test passes.
|
||||
'social-media-dashboard',
|
||||
'social-media-matrix-tracker-template',
|
||||
'8-bit-orbit-video-template',
|
||||
'digits-fintech-swiss-template',
|
||||
'field-notes-editorial-template',
|
||||
'html-ppt-retro-quarterly-review',
|
||||
'swiss-user-research-video-template',
|
||||
'web-prototype-taste-brutalist',
|
||||
'web-prototype-taste-editorial',
|
||||
'web-prototype-taste-soft',
|
||||
'waitlist-page',
|
||||
'x-research',
|
||||
'trading-analysis-dashboard-template',
|
||||
'swiss-creative-mode-template',
|
||||
'github-dashboard',
|
||||
'after-hours-editorial-template',
|
||||
// Curated design/creative skill catalogue (PR #955) — lightweight stubs
|
||||
// pointing at upstream awesome-claude-skills / awesome-agent-skills
|
||||
// entries; English-only by design. The localized-content coverage test
|
||||
// treats these as English-fallback so the stubs land in Settings →
|
||||
// Skills without forcing a localization task per locale.
|
||||
'ad-creative',
|
||||
'ai-music-album',
|
||||
'algorithmic-art',
|
||||
'apple-hig',
|
||||
'artifacts-builder',
|
||||
'brainstorming',
|
||||
'brand-guidelines',
|
||||
'canvas-design',
|
||||
'color-expert',
|
||||
'competitive-ads-extractor',
|
||||
'copywriting',
|
||||
'creative-director',
|
||||
'd3-visualization',
|
||||
'design-consultation',
|
||||
'design-md',
|
||||
'design-review',
|
||||
'doc',
|
||||
'docx',
|
||||
'domain-name-brainstormer',
|
||||
'enhance-prompt',
|
||||
'fal-3d',
|
||||
'fal-generate',
|
||||
'fal-image-edit',
|
||||
'fal-kling-o3',
|
||||
'fal-lip-sync',
|
||||
'fal-realtime',
|
||||
'fal-restore',
|
||||
'fal-train',
|
||||
'fal-tryon',
|
||||
'fal-upscale',
|
||||
'fal-video-edit',
|
||||
'fal-vision',
|
||||
'figma-code-connect-components',
|
||||
'figma-create-design-system-rules',
|
||||
'figma-create-new-file',
|
||||
'figma-generate-design',
|
||||
'figma-generate-library',
|
||||
'figma-implement-design',
|
||||
'figma-use',
|
||||
'flutter-animating-apps',
|
||||
'frontend-design',
|
||||
'frontend-dev',
|
||||
'frontend-skill',
|
||||
'frontend-slides',
|
||||
'full-page-screenshot',
|
||||
'gif-sticker-maker',
|
||||
'gsap-core',
|
||||
'gsap-react',
|
||||
'gsap-scrolltrigger',
|
||||
'gsap-timeline',
|
||||
'hand-drawn-diagrams',
|
||||
'image-enhancer',
|
||||
'imagegen',
|
||||
'imagen',
|
||||
'marketing-psychology',
|
||||
'minimax-docx',
|
||||
'minimax-pdf',
|
||||
'nanobanana-ppt',
|
||||
'paywall-upgrade-cro',
|
||||
'pdf',
|
||||
'pixelbin-media',
|
||||
'plan-design-review',
|
||||
'platform-design',
|
||||
'pptx',
|
||||
'pptx-generator',
|
||||
'remotion',
|
||||
'replicate',
|
||||
'screenshot',
|
||||
'screenshots-marketing',
|
||||
'shadcn-ui',
|
||||
'shader-dev',
|
||||
'slack-gif-creator',
|
||||
'slides',
|
||||
'sora',
|
||||
'speech',
|
||||
'stitch-loop',
|
||||
'swiftui-design',
|
||||
'taste-skill',
|
||||
'theme-factory',
|
||||
'threejs',
|
||||
'ui-skills',
|
||||
'ui-ux-pro-max',
|
||||
'venice-audio-music',
|
||||
'venice-audio-speech',
|
||||
'venice-image-edit',
|
||||
'venice-image-generate',
|
||||
'venice-video',
|
||||
'video-downloader',
|
||||
'web-artifacts-builder',
|
||||
'web-design-guidelines',
|
||||
'wpds',
|
||||
'youtube-clipper',
|
||||
] as const;
|
||||
|
||||
export const RU_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [
|
||||
'agentic',
|
||||
'ant',
|
||||
'application',
|
||||
'arc',
|
||||
'artistic',
|
||||
'bento',
|
||||
'bmw-m',
|
||||
'bold',
|
||||
'brutalism',
|
||||
'cafe',
|
||||
'canva',
|
||||
'cisco',
|
||||
'claymorphism',
|
||||
'clean',
|
||||
'colorful',
|
||||
'contemporary',
|
||||
'corporate',
|
||||
'cosmic',
|
||||
'creative',
|
||||
'dashboard',
|
||||
'discord',
|
||||
'dithered',
|
||||
'doodle',
|
||||
'dramatic',
|
||||
'duolingo',
|
||||
'editorial',
|
||||
'elegant',
|
||||
'energetic',
|
||||
'enterprise',
|
||||
'expressive',
|
||||
'fantasy',
|
||||
'flat',
|
||||
'friendly',
|
||||
'futuristic',
|
||||
'github',
|
||||
'glassmorphism',
|
||||
'gradient',
|
||||
'huggingface',
|
||||
'hud',
|
||||
'levels',
|
||||
'lingo',
|
||||
'luxury',
|
||||
'material',
|
||||
'minimal',
|
||||
'mission-control',
|
||||
'modern',
|
||||
'mono',
|
||||
'neobrutalism',
|
||||
'neon',
|
||||
'neumorphism',
|
||||
'openai',
|
||||
'pacman',
|
||||
'paper',
|
||||
'perspective',
|
||||
'premium',
|
||||
'professional',
|
||||
'publication',
|
||||
'refined',
|
||||
'retro',
|
||||
'shadcn',
|
||||
'simple',
|
||||
'skeumorphism',
|
||||
'slack',
|
||||
'sleek',
|
||||
'spacious',
|
||||
'storytelling',
|
||||
'totality-festival',
|
||||
'tetris',
|
||||
'urdu',
|
||||
'vibrant',
|
||||
'vintage',
|
||||
'wechat',
|
||||
'webex',
|
||||
] as const;
|
||||
|
||||
export const RU_PROMPT_TEMPLATE_CATEGORIES: Record<string, string> = {
|
||||
Infographic: 'Инфографика',
|
||||
'Anime / Manga': 'Аниме / манга',
|
||||
|
|
@ -593,17 +348,6 @@ export const RU_PROMPT_TEMPLATE_CATEGORIES: Record<string, string> = {
|
|||
'VFX / HTML-in-Canvas': 'VFX / HTML-in-Canvas',
|
||||
};
|
||||
|
||||
export const RU_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK = [
|
||||
'notion-team-dashboard-live-artifact',
|
||||
'hyperframes-html-in-canvas-iphone-device',
|
||||
'hyperframes-html-in-canvas-liquid-background',
|
||||
'hyperframes-html-in-canvas-liquid-glass',
|
||||
'hyperframes-html-in-canvas-magnetic',
|
||||
'hyperframes-html-in-canvas-portal-reveal',
|
||||
'hyperframes-html-in-canvas-shatter',
|
||||
'hyperframes-html-in-canvas-text-cursor',
|
||||
] as const;
|
||||
|
||||
export const RU_PROMPT_TEMPLATE_TAGS: Record<string, string> = {
|
||||
'3d': '3D',
|
||||
'3d-render': '3D-рендер',
|
||||
|
|
|
|||
|
|
@ -6,25 +6,19 @@ import type {
|
|||
import type { Locale } from './types';
|
||||
import {
|
||||
FR_DESIGN_SYSTEM_CATEGORIES,
|
||||
FR_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK,
|
||||
FR_DESIGN_SYSTEM_SUMMARIES,
|
||||
FR_PROMPT_TEMPLATE_CATEGORIES,
|
||||
FR_PROMPT_TEMPLATE_COPY,
|
||||
FR_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK,
|
||||
FR_PROMPT_TEMPLATE_TAGS,
|
||||
FR_SKILL_COPY,
|
||||
FR_SKILL_IDS_WITH_EN_FALLBACK,
|
||||
} from './content.fr';
|
||||
import {
|
||||
RU_DESIGN_SYSTEM_CATEGORIES,
|
||||
RU_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK,
|
||||
RU_DESIGN_SYSTEM_SUMMARIES,
|
||||
RU_PROMPT_TEMPLATE_CATEGORIES,
|
||||
RU_PROMPT_TEMPLATE_COPY,
|
||||
RU_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK,
|
||||
RU_PROMPT_TEMPLATE_TAGS,
|
||||
RU_SKILL_COPY,
|
||||
RU_SKILL_IDS_WITH_EN_FALLBACK,
|
||||
} from './content.ru';
|
||||
|
||||
type LocalizedSkillCopy = { description?: string; examplePrompt?: string };
|
||||
|
|
@ -39,12 +33,9 @@ type LocalizedContentIds = {
|
|||
};
|
||||
type LocalizedContentBundle = {
|
||||
skillCopy: Record<string, LocalizedSkillCopy>;
|
||||
skillIdsWithEnFallback: readonly string[];
|
||||
designSystemSummaries: Record<string, string>;
|
||||
designSystemCategories: Record<string, string>;
|
||||
designSystemIdsWithEnFallback: readonly string[];
|
||||
promptTemplateCategories: Record<string, string>;
|
||||
promptTemplateIdsWithEnFallback: readonly string[];
|
||||
promptTemplateTags: Record<string, string>;
|
||||
promptTemplateCopy: Record<string, LocalizedPromptTemplateCopy>;
|
||||
};
|
||||
|
|
@ -370,254 +361,6 @@ const DE_DESIGN_SYSTEM_CATEGORIES: Record<string, string> = {
|
|||
Uncategorized: 'Nicht kategorisiert',
|
||||
};
|
||||
|
||||
const DE_SKILL_IDS_WITH_EN_FALLBACK = [
|
||||
'clinical-case-report',
|
||||
'dcf-valuation',
|
||||
'editorial-burgundy-principles-template',
|
||||
'flowai-live-dashboard-template',
|
||||
'html-ppt-taste-brutalist',
|
||||
'html-ppt-taste-editorial',
|
||||
// Vendored upstream English-language Zara templates (zarazhangrui/beautiful-html-templates).
|
||||
// Localized copy is not maintained; fall back to the upstream English description.
|
||||
'html-ppt-zhangzara-8-bit-orbit',
|
||||
'html-ppt-zhangzara-biennale-yellow',
|
||||
'html-ppt-zhangzara-block-frame',
|
||||
'html-ppt-zhangzara-blue-professional',
|
||||
'html-ppt-zhangzara-bold-poster',
|
||||
'html-ppt-zhangzara-broadside',
|
||||
'html-ppt-zhangzara-capsule',
|
||||
'html-ppt-zhangzara-cartesian',
|
||||
'html-ppt-zhangzara-cobalt-grid',
|
||||
'html-ppt-zhangzara-coral',
|
||||
'html-ppt-zhangzara-creative-mode',
|
||||
'html-ppt-zhangzara-daisy-days',
|
||||
'html-ppt-zhangzara-editorial-tri-tone',
|
||||
'html-ppt-zhangzara-grove',
|
||||
'html-ppt-zhangzara-long-table',
|
||||
'html-ppt-zhangzara-mat',
|
||||
'html-ppt-zhangzara-monochrome',
|
||||
'html-ppt-zhangzara-neo-grid-bold',
|
||||
'html-ppt-zhangzara-peoples-platform',
|
||||
'html-ppt-zhangzara-pin-and-paper',
|
||||
'html-ppt-zhangzara-pink-script',
|
||||
'html-ppt-zhangzara-playful',
|
||||
'html-ppt-zhangzara-raw-grid',
|
||||
'html-ppt-zhangzara-retro-windows',
|
||||
'html-ppt-zhangzara-retro-zine',
|
||||
'html-ppt-zhangzara-sakura-chroma',
|
||||
'html-ppt-zhangzara-scatterbrain',
|
||||
'html-ppt-zhangzara-signal',
|
||||
'html-ppt-zhangzara-soft-editorial',
|
||||
'html-ppt-zhangzara-stencil-tablet',
|
||||
'html-ppt-zhangzara-studio',
|
||||
'html-ppt-zhangzara-vellum',
|
||||
// IB pitch-book skill (#888): English-only skill copy for now.
|
||||
'ib-pitch-book',
|
||||
'last30days',
|
||||
'live-dashboard',
|
||||
'login-flow',
|
||||
'orbit-general',
|
||||
'orbit-github',
|
||||
'orbit-gmail',
|
||||
'orbit-linear',
|
||||
'orbit-notion',
|
||||
'release-notes-one-pager',
|
||||
// TODO: add localized copy for social-media-dashboard (introduced in #678).
|
||||
// Fallback for now so the localized-content coverage test passes.
|
||||
'social-media-dashboard',
|
||||
'social-media-matrix-tracker-template',
|
||||
'8-bit-orbit-video-template',
|
||||
'digits-fintech-swiss-template',
|
||||
'field-notes-editorial-template',
|
||||
'html-ppt-retro-quarterly-review',
|
||||
'swiss-user-research-video-template',
|
||||
'web-prototype-taste-brutalist',
|
||||
'web-prototype-taste-editorial',
|
||||
'web-prototype-taste-soft',
|
||||
'waitlist-page',
|
||||
'x-research',
|
||||
'trading-analysis-dashboard-template',
|
||||
'swiss-creative-mode-template',
|
||||
'github-dashboard',
|
||||
'after-hours-editorial-template',
|
||||
// Curated design/creative skill catalogue (PR #955) — lightweight stubs
|
||||
// that point at upstream awesome-claude-skills / awesome-agent-skills
|
||||
// entries. The frontmatter description is English-only by design; the
|
||||
// localized-content coverage test treats these as English-fallback so
|
||||
// the stubs land in Settings → Skills without forcing a localization
|
||||
// task per locale.
|
||||
'ad-creative',
|
||||
'ai-music-album',
|
||||
'algorithmic-art',
|
||||
'apple-hig',
|
||||
'artifacts-builder',
|
||||
'brainstorming',
|
||||
'brand-guidelines',
|
||||
'canvas-design',
|
||||
'color-expert',
|
||||
'competitive-ads-extractor',
|
||||
'copywriting',
|
||||
'creative-director',
|
||||
'd3-visualization',
|
||||
'design-consultation',
|
||||
'design-md',
|
||||
'design-review',
|
||||
'doc',
|
||||
'docx',
|
||||
'domain-name-brainstormer',
|
||||
'enhance-prompt',
|
||||
'fal-3d',
|
||||
'fal-generate',
|
||||
'fal-image-edit',
|
||||
'fal-kling-o3',
|
||||
'fal-lip-sync',
|
||||
'fal-realtime',
|
||||
'fal-restore',
|
||||
'fal-train',
|
||||
'fal-tryon',
|
||||
'fal-upscale',
|
||||
'fal-video-edit',
|
||||
'fal-vision',
|
||||
'figma-code-connect-components',
|
||||
'figma-create-design-system-rules',
|
||||
'figma-create-new-file',
|
||||
'figma-generate-design',
|
||||
'figma-generate-library',
|
||||
'figma-implement-design',
|
||||
'figma-use',
|
||||
'flutter-animating-apps',
|
||||
'frontend-design',
|
||||
'frontend-dev',
|
||||
'frontend-skill',
|
||||
'frontend-slides',
|
||||
'full-page-screenshot',
|
||||
'gif-sticker-maker',
|
||||
'gsap-core',
|
||||
'gsap-react',
|
||||
'gsap-scrolltrigger',
|
||||
'gsap-timeline',
|
||||
'hand-drawn-diagrams',
|
||||
'image-enhancer',
|
||||
'imagegen',
|
||||
'imagen',
|
||||
'marketing-psychology',
|
||||
'minimax-docx',
|
||||
'minimax-pdf',
|
||||
'nanobanana-ppt',
|
||||
'paywall-upgrade-cro',
|
||||
'pdf',
|
||||
'pixelbin-media',
|
||||
'plan-design-review',
|
||||
'platform-design',
|
||||
'pptx',
|
||||
'pptx-generator',
|
||||
'remotion',
|
||||
'replicate',
|
||||
'screenshot',
|
||||
'screenshots-marketing',
|
||||
'shadcn-ui',
|
||||
'shader-dev',
|
||||
'slack-gif-creator',
|
||||
'slides',
|
||||
'sora',
|
||||
'speech',
|
||||
'stitch-loop',
|
||||
'swiftui-design',
|
||||
'taste-skill',
|
||||
'theme-factory',
|
||||
'threejs',
|
||||
'ui-skills',
|
||||
'ui-ux-pro-max',
|
||||
'venice-audio-music',
|
||||
'venice-audio-speech',
|
||||
'venice-image-edit',
|
||||
'venice-image-generate',
|
||||
'venice-video',
|
||||
'video-downloader',
|
||||
'web-artifacts-builder',
|
||||
'web-design-guidelines',
|
||||
'wpds',
|
||||
'youtube-clipper',
|
||||
] as const;
|
||||
|
||||
const DE_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [
|
||||
'agentic',
|
||||
'ant',
|
||||
'application',
|
||||
'arc',
|
||||
'artistic',
|
||||
'bento',
|
||||
'bmw-m',
|
||||
'bold',
|
||||
'brutalism',
|
||||
'cafe',
|
||||
'canva',
|
||||
'cisco',
|
||||
'claymorphism',
|
||||
'clean',
|
||||
'colorful',
|
||||
'contemporary',
|
||||
'corporate',
|
||||
'cosmic',
|
||||
'creative',
|
||||
'dashboard',
|
||||
'discord',
|
||||
'dithered',
|
||||
'doodle',
|
||||
'dramatic',
|
||||
'duolingo',
|
||||
'editorial',
|
||||
'elegant',
|
||||
'energetic',
|
||||
'enterprise',
|
||||
'expressive',
|
||||
'fantasy',
|
||||
'flat',
|
||||
'friendly',
|
||||
'futuristic',
|
||||
'github',
|
||||
'glassmorphism',
|
||||
'gradient',
|
||||
'huggingface',
|
||||
'hud',
|
||||
'levels',
|
||||
'lingo',
|
||||
'loom',
|
||||
'luxury',
|
||||
'material',
|
||||
'minimal',
|
||||
'mission-control',
|
||||
'modern',
|
||||
'mono',
|
||||
'neobrutalism',
|
||||
'neon',
|
||||
'neumorphism',
|
||||
'openai',
|
||||
'pacman',
|
||||
'paper',
|
||||
'perspective',
|
||||
'premium',
|
||||
'professional',
|
||||
'publication',
|
||||
'refined',
|
||||
'retro',
|
||||
'shadcn',
|
||||
'simple',
|
||||
'skeumorphism',
|
||||
'slack',
|
||||
'sleek',
|
||||
'spacious',
|
||||
'storytelling',
|
||||
'totality-festival',
|
||||
'tetris',
|
||||
'trading-terminal',
|
||||
'urdu',
|
||||
'vibrant',
|
||||
'vintage',
|
||||
'webex',
|
||||
'wechat',
|
||||
] as const;
|
||||
|
||||
const DE_PROMPT_TEMPLATE_CATEGORIES: Record<string, string> = {
|
||||
Infographic: 'Infografik',
|
||||
'Anime / Manga': 'Anime / Manga',
|
||||
|
|
@ -643,17 +386,6 @@ const DE_PROMPT_TEMPLATE_CATEGORIES: Record<string, string> = {
|
|||
'VFX / HTML-in-Canvas': 'VFX / HTML-in-Canvas',
|
||||
};
|
||||
|
||||
const DE_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK = [
|
||||
'notion-team-dashboard-live-artifact',
|
||||
'hyperframes-html-in-canvas-iphone-device',
|
||||
'hyperframes-html-in-canvas-liquid-background',
|
||||
'hyperframes-html-in-canvas-liquid-glass',
|
||||
'hyperframes-html-in-canvas-magnetic',
|
||||
'hyperframes-html-in-canvas-portal-reveal',
|
||||
'hyperframes-html-in-canvas-shatter',
|
||||
'hyperframes-html-in-canvas-text-cursor',
|
||||
] as const;
|
||||
|
||||
const DE_PROMPT_TEMPLATE_TAGS: Record<string, string> = {
|
||||
'3d': '3D',
|
||||
'3d-render': '3D-Render',
|
||||
|
|
@ -1218,34 +950,25 @@ const DE_PROMPT_TEMPLATE_COPY: Record<string, LocalizedPromptTemplateCopy> = {
|
|||
const LOCALIZED_CONTENT: Partial<Record<Locale, LocalizedContentBundle>> = {
|
||||
de: {
|
||||
skillCopy: DE_SKILL_COPY,
|
||||
skillIdsWithEnFallback: DE_SKILL_IDS_WITH_EN_FALLBACK,
|
||||
designSystemSummaries: DE_DESIGN_SYSTEM_SUMMARIES,
|
||||
designSystemCategories: DE_DESIGN_SYSTEM_CATEGORIES,
|
||||
designSystemIdsWithEnFallback: DE_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK,
|
||||
promptTemplateCategories: DE_PROMPT_TEMPLATE_CATEGORIES,
|
||||
promptTemplateIdsWithEnFallback: DE_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK,
|
||||
promptTemplateTags: DE_PROMPT_TEMPLATE_TAGS,
|
||||
promptTemplateCopy: DE_PROMPT_TEMPLATE_COPY,
|
||||
},
|
||||
ru: {
|
||||
skillCopy: RU_SKILL_COPY,
|
||||
skillIdsWithEnFallback: RU_SKILL_IDS_WITH_EN_FALLBACK,
|
||||
designSystemSummaries: RU_DESIGN_SYSTEM_SUMMARIES,
|
||||
designSystemCategories: RU_DESIGN_SYSTEM_CATEGORIES,
|
||||
designSystemIdsWithEnFallback: RU_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK,
|
||||
promptTemplateCategories: RU_PROMPT_TEMPLATE_CATEGORIES,
|
||||
promptTemplateIdsWithEnFallback: RU_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK,
|
||||
promptTemplateTags: RU_PROMPT_TEMPLATE_TAGS,
|
||||
promptTemplateCopy: RU_PROMPT_TEMPLATE_COPY,
|
||||
},
|
||||
fr: {
|
||||
skillCopy: FR_SKILL_COPY,
|
||||
skillIdsWithEnFallback: FR_SKILL_IDS_WITH_EN_FALLBACK,
|
||||
designSystemSummaries: FR_DESIGN_SYSTEM_SUMMARIES,
|
||||
designSystemCategories: FR_DESIGN_SYSTEM_CATEGORIES,
|
||||
designSystemIdsWithEnFallback: FR_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK,
|
||||
promptTemplateCategories: FR_PROMPT_TEMPLATE_CATEGORIES,
|
||||
promptTemplateIdsWithEnFallback: FR_PROMPT_TEMPLATE_IDS_WITH_EN_FALLBACK,
|
||||
promptTemplateTags: FR_PROMPT_TEMPLATE_TAGS,
|
||||
promptTemplateCopy: FR_PROMPT_TEMPLATE_COPY,
|
||||
},
|
||||
|
|
@ -1253,19 +976,10 @@ const LOCALIZED_CONTENT: Partial<Record<Locale, LocalizedContentBundle>> = {
|
|||
|
||||
function buildLocalizedContentIds(content: LocalizedContentBundle): LocalizedContentIds {
|
||||
return {
|
||||
skills: [
|
||||
...Object.keys(content.skillCopy),
|
||||
...content.skillIdsWithEnFallback,
|
||||
],
|
||||
designSystems: [
|
||||
...Object.keys(content.designSystemSummaries),
|
||||
...content.designSystemIdsWithEnFallback,
|
||||
],
|
||||
skills: Object.keys(content.skillCopy),
|
||||
designSystems: Object.keys(content.designSystemSummaries),
|
||||
designSystemCategories: Object.keys(content.designSystemCategories),
|
||||
promptTemplates: [
|
||||
...Object.keys(content.promptTemplateCopy),
|
||||
...content.promptTemplateIdsWithEnFallback,
|
||||
],
|
||||
promptTemplates: Object.keys(content.promptTemplateCopy),
|
||||
promptTemplateCategories: Object.keys(content.promptTemplateCategories),
|
||||
promptTemplateTags: Object.keys(content.promptTemplateTags),
|
||||
};
|
||||
|
|
|
|||
81
apps/web/tests/i18n/content.test.ts
Normal file
81
apps/web/tests/i18n/content.test.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type { DesignSystemSummary, PromptTemplateSummary, SkillSummary } from '../../src/types';
|
||||
import {
|
||||
FRENCH_CONTENT_IDS,
|
||||
localizeDesignSystemSummary,
|
||||
localizePromptTemplateSummary,
|
||||
localizeSkillDescription,
|
||||
localizeSkillPrompt,
|
||||
} from '../../src/i18n/content';
|
||||
|
||||
describe('localized resource content', () => {
|
||||
it('derives localized ids only from localized dictionaries', () => {
|
||||
expect(FRENCH_CONTENT_IDS.skills).toContain('blog-post');
|
||||
expect(FRENCH_CONTENT_IDS.skills).not.toContain('ib-pitch-book');
|
||||
expect(FRENCH_CONTENT_IDS.designSystems).toContain('airbnb');
|
||||
expect(FRENCH_CONTENT_IDS.designSystems).not.toContain('agentic');
|
||||
expect(FRENCH_CONTENT_IDS.promptTemplates).toContain('3d-stone-staircase-evolution-infographic');
|
||||
expect(FRENCH_CONTENT_IDS.promptTemplates).not.toContain('notion-team-dashboard-live-artifact');
|
||||
});
|
||||
|
||||
it('prefers localized skill copy and falls back to english field-by-field', () => {
|
||||
const partiallyLocalizedSkill = {
|
||||
id: 'blog-post',
|
||||
examplePrompt: ' English prompt from source. ',
|
||||
description: ' English description from source. ',
|
||||
} as SkillSummary;
|
||||
|
||||
expect(localizeSkillPrompt('fr', partiallyLocalizedSkill)).toBe(
|
||||
'Un article long-form / blog post — masthead, placeholder d’image hero, corps d’article avec figures et pull quotes, ligne auteur, articles associés.',
|
||||
);
|
||||
expect(localizeSkillDescription('fr', partiallyLocalizedSkill)).toBe(
|
||||
'English description from source.',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to english design system summaries when localized copy is missing', () => {
|
||||
const englishOnlySystem = {
|
||||
id: 'agentic',
|
||||
summary: ' English summary from source. ',
|
||||
category: 'English category',
|
||||
} as DesignSystemSummary;
|
||||
|
||||
expect(localizeDesignSystemSummary('fr', englishOnlySystem)).toBe(' English summary from source. ');
|
||||
});
|
||||
|
||||
it('prefers localized prompt template fields and falls back to english fields and tags', () => {
|
||||
const translatedTemplate = {
|
||||
id: '3d-stone-staircase-evolution-infographic',
|
||||
surface: 'image',
|
||||
title: 'English title',
|
||||
summary: 'English summary',
|
||||
category: 'Infographic',
|
||||
tags: ['3d', 'unknown-tag'],
|
||||
source: { repo: 'repo', license: 'MIT' },
|
||||
} satisfies PromptTemplateSummary;
|
||||
|
||||
const localized = localizePromptTemplateSummary('fr', translatedTemplate);
|
||||
expect(localized.title).toBe('Infographie 3D d’une évolution en escalier de pierre');
|
||||
expect(localized.summary).toBe(
|
||||
'Transforme une timeline d’évolution plate en infographie 3D réaliste en escalier de pierre, avec rendus détaillés d’organismes et panneaux latéraux structurés.',
|
||||
);
|
||||
expect(localized.category).toBe('Infographie');
|
||||
expect(localized.tags).toEqual(['3D', 'unknown-tag']);
|
||||
|
||||
const englishOnlyTemplate = {
|
||||
...translatedTemplate,
|
||||
id: 'notion-team-dashboard-live-artifact',
|
||||
title: ' English title from source ',
|
||||
summary: ' English summary from source ',
|
||||
category: 'General',
|
||||
tags: ['unknown-tag'],
|
||||
} satisfies PromptTemplateSummary;
|
||||
|
||||
expect(localizePromptTemplateSummary('fr', englishOnlyTemplate)).toMatchObject({
|
||||
title: ' English title from source ',
|
||||
summary: ' English summary from source ',
|
||||
category: 'Général',
|
||||
tags: ['unknown-tag'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -250,27 +250,27 @@ Match easing to purpose: `ease-in` for entering, `ease-out` for leaving, `linear
|
|||
|
||||
## 7. Locale Coverage Requirements
|
||||
|
||||
When adding a new design system, you must also add its localized catalog entry. This is a **separate PR** (do not mix design system and i18n changes).
|
||||
When adding a new design system, include complete English catalog metadata in `design-systems/<id>/DESIGN.md`. Locales use translated summaries when present and otherwise derive the runtime fallback from the English source fields.
|
||||
|
||||
### Which locales need updating?
|
||||
### Which localized dictionaries need updating?
|
||||
|
||||
Use this decision tree to decide which array to add to:
|
||||
Use this decision tree to decide whether to add dictionary copy:
|
||||
|
||||
**Does a localized summary already exist for this design system?**
|
||||
- **Yes** → Add to `*_DESIGN_SYSTEM_SUMMARIES` only (FR + RU). **Never also add to the fallback array** — `buildLocalizedContentIds()` concatenates both without deduplication, producing a duplicate that fails the test.
|
||||
- **No** (no translation yet) → Add to `*_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK` only. This makes the system appear in the catalog with English fallbacks.
|
||||
- **Yes** → Add it to the matching `*_DESIGN_SYSTEM_SUMMARIES` dictionary.
|
||||
- **No** (no translation yet) → Keep the English `summary` and `category` metadata complete in `DESIGN.md`; the localized runtime renders those fields through the default fallback path.
|
||||
|
||||
| Locale | File to update | Array |
|
||||
|--------|---------------|-------|
|
||||
| German | `apps/web/src/i18n/content.ts` | `DE_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK` |
|
||||
| French | `apps/web/src/i18n/content.fr.ts` | `FR_DESIGN_SYSTEM_SUMMARIES` (if localized copy exists) or `FR_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK` (if not) |
|
||||
| Russian | `apps/web/src/i18n/content.ru.ts` | `RU_DESIGN_SYSTEM_SUMMARIES` (if localized copy exists) or `RU_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK` (if not) |
|
||||
| German | `apps/web/src/i18n/content.ts` | `DE_DESIGN_SYSTEM_SUMMARIES` when localized copy exists |
|
||||
| French | `apps/web/src/i18n/content.fr.ts` | `FR_DESIGN_SYSTEM_SUMMARIES` when localized copy exists |
|
||||
| Russian | `apps/web/src/i18n/content.ru.ts` | `RU_DESIGN_SYSTEM_SUMMARIES` when localized copy exists |
|
||||
|
||||
> ⚠️ **The duplicate-ID trap:** `buildLocalizedContentIds()` concatenates summary keys and fallback IDs **without deduplication**. Adding to both arrays produces the same ID twice, and the e2e test's curated ID list rejects duplicates. This is exactly what caused the loom/trading-terminal #929 failure.
|
||||
The default English fallback path is automatic. Add localized summary dictionaries only when translated copy exists.
|
||||
|
||||
### Test behavior
|
||||
|
||||
The `e2e/tests/localized-content.test.ts` test verifies that every `design-systems/*/DESIGN.md` on disk has a corresponding catalog entry. If your design system directory does not exist in the locale PR's branch, do not add it to the DE fallback array — the test builds the expected ID list from the filesystem.
|
||||
The `e2e/tests/localized-content.test.ts` test verifies that every `design-systems/*/DESIGN.md` on disk is discoverable and renders a non-empty localized summary through either translated dictionary copy or the English fallback fields.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -325,4 +325,4 @@ A well-documented design system is typically 300–600 lines. Being too brief (u
|
|||
- Visual Theme: 30–40 lines (atmosphere + use cases + prior art)
|
||||
- Anti-patterns: 8–15 lines (one per key mistake to avoid)
|
||||
|
||||
The mission-control design system (`design-systems/mission-control/DESIGN.md`) is a good reference — tight scope (3 primary colors, dark only, 6 components).
|
||||
The mission-control design system (`design-systems/mission-control/DESIGN.md`) is a good reference — tight scope (3 primary colors, dark only, 6 components).
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ We hold skill PRs to a higher bar than feature PRs because skills are the user-f
|
|||
|
||||
### Shape
|
||||
|
||||
- [ ] **Single self-contained folder + the i18n fallback line.** Everything the skill needs lives under `skills/<your-skill>/`. The **only** outside edit is adding your skill id to the `*_SKILL_IDS_WITH_EN_FALLBACK` arrays — see "i18n coverage" below. No edits to `apps/daemon/`, `packages/`, `tools/`, etc. in the same PR.
|
||||
- [ ] **Single self-contained folder + discoverable English display copy.** Everything the skill needs lives under `skills/<your-skill>/`. The folder's `SKILL.md` must include the English display fields consumed by the picker — see "i18n coverage" below. No edits to `apps/daemon/`, `packages/`, `tools/`, etc. in the same PR.
|
||||
- [ ] **No CDN imports** beyond what other skills already use. If you need a new font CDN, GSAP, three.js, etc., raise it in your PR description.
|
||||
- [ ] **No images larger than ~250 KB.** If your example genuinely needs a hero photo, run it through an optimizer first. No raw PNG screenshots.
|
||||
- [ ] **No fonts you didn't license.** System font stack is always safe; Google Fonts and Adobe Fonts free tier are also safe; anything else needs a license file in `references/`.
|
||||
|
|
@ -187,19 +187,19 @@ We hold skill PRs to a higher bar than feature PRs because skills are the user-f
|
|||
|
||||
### i18n coverage (every skill, not just featured)
|
||||
|
||||
The `e2e/tests/localized-content.test.ts` test enforces that every directory under `skills/` with a `SKILL.md` is represented in the localized content metadata for de / ru / fr — otherwise CI fails on the `skills display copy` assertion.
|
||||
The `e2e/tests/localized-content.test.ts` test enforces that every directory under `skills/` with a `SKILL.md` is discoverable and displayable for de / ru / fr. Locales use translated copy when present and otherwise derive the runtime fallback from the English source fields in `SKILL.md`.
|
||||
|
||||
For a non-featured skill, the cheap path is to declare your id falls back to English:
|
||||
For a non-featured skill, the cheap path is to keep the source metadata complete:
|
||||
|
||||
- [ ] **Add your skill id to all three `*_SKILL_IDS_WITH_EN_FALLBACK` arrays** in `apps/web/src/i18n/content.ts` (DE), `apps/web/src/i18n/content.fr.ts` (FR), and `apps/web/src/i18n/content.ru.ts` (RU). Just the bare id on its own line, sorted alphabetically — **no `TODO:` comment**, no inline note. The fallback marker IS the note.
|
||||
- [ ] **Run `pnpm --filter @open-design/web test`** locally before pushing. The localized-content test catches missing entries; failing it earns a "please add the fallback line" comment.
|
||||
- [ ] **Ensure `SKILL.md` has complete English display copy**: title/name, description, example prompt, and any picker metadata required by the skill schema. The localized runtime uses these fields as the fallback display path.
|
||||
- [ ] **Run `pnpm --filter @open-design/web test` and `pnpm --filter @open-design/e2e test tests/localized-content.test.ts`** locally before pushing. These suites catch missing localized dictionaries and undisplayable discovered resources.
|
||||
|
||||
### Featured skills (optional path)
|
||||
|
||||
If you set `od.featured: 1`, also:
|
||||
|
||||
- [ ] **Add a screenshot** at `docs/screenshots/skills/<skill>.png`. PNG, ~1024×640 retina, captured from the real `example.html` at zoomed-out browser scale.
|
||||
- [ ] **Replace the fallback id with full localized display copy** in `content.ts` (DE), `content.fr.ts` (FR), `content.ru.ts` (RU) — title, summary, scenario tag. The featured row in the picker uses this copy; the bare fallback path renders English everywhere.
|
||||
- [ ] **Add full localized display copy** in `content.ts` (DE), `content.fr.ts` (FR), `content.ru.ts` (RU) — title, summary, scenario tag. The featured row in the picker uses this copy; the default fallback path renders English everywhere.
|
||||
|
||||
### Forking
|
||||
|
||||
|
|
@ -234,8 +234,8 @@ they don't cover this case. If you can't, fold into the existing skill instead.
|
|||
- [ ] Sent the `example_prompt` end-to-end and confirmed the artifact rendered
|
||||
- [ ] Verified export works (PPTX / PDF / etc.) if the mode supports it
|
||||
- [ ] Ran `pnpm typecheck`
|
||||
- [ ] Added the skill id to all three `*_SKILL_IDS_WITH_EN_FALLBACK` arrays (or full localized copy if featured) — **required for every skill**
|
||||
- [ ] Ran `pnpm --filter @open-design/web test` and the `localized-content` suite is green
|
||||
- [ ] Verified `SKILL.md` has complete English display copy for localized fallback — **required for every skill**
|
||||
- [ ] Ran `pnpm --filter @open-design/web test` and `pnpm --filter @open-design/e2e test tests/localized-content.test.ts`; localized-content coverage is green
|
||||
|
||||
## Screenshot
|
||||
(Required if `od.featured` is set. Otherwise nice-to-have.)
|
||||
|
|
|
|||
|
|
@ -21,8 +21,18 @@ type LocalizedContentIds = {
|
|||
|
||||
type LocalizedContentModule = {
|
||||
LOCALIZED_CONTENT_IDS: Record<string, LocalizedContentIds>;
|
||||
localizeDesignSystemSummary: (locale: string, system: DesignSystemResource) => string;
|
||||
localizePromptTemplateSummary: (
|
||||
locale: string,
|
||||
template: PromptTemplateResource,
|
||||
) => PromptTemplateResource;
|
||||
localizeSkillDescription: (locale: string, skill: SkillResource) => string;
|
||||
};
|
||||
|
||||
type SkillResource = { id: string; description: string };
|
||||
type DesignSystemResource = { id: string; category: string; summary: string | null };
|
||||
type PromptTemplateResource = { id: string; category: string; tags: string[]; title: string; summary: string };
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('../../', import.meta.url));
|
||||
const webContentModules = import.meta.glob<LocalizedContentModule>(
|
||||
'../../apps/web/src/i18n/content.ts',
|
||||
|
|
@ -34,7 +44,14 @@ if (localizedContentModule == null) {
|
|||
throw new Error('Failed to load apps/web localized content ids');
|
||||
}
|
||||
|
||||
const { LOCALIZED_CONTENT_IDS } = localizedContentModule;
|
||||
const {
|
||||
LOCALIZED_CONTENT_IDS,
|
||||
localizeDesignSystemSummary,
|
||||
localizePromptTemplateSummary,
|
||||
localizeSkillDescription,
|
||||
} = localizedContentModule;
|
||||
const COVERAGE_LOCALES = ['de', 'fr', 'ru'] as const;
|
||||
const RESOURCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
||||
|
||||
function sorted(values: Iterable<string>): string[] {
|
||||
return [...values].sort((a, b) => a.localeCompare(b));
|
||||
|
|
@ -44,118 +61,84 @@ function uniqueSorted(values: Iterable<string>): string[] {
|
|||
return sorted(new Set(values));
|
||||
}
|
||||
|
||||
function findMissingIds(localizedIds: Iterable<string>, discoveredIds: Iterable<string>): string[] {
|
||||
const localized = new Set(localizedIds);
|
||||
const discovered = new Set(discoveredIds);
|
||||
return sorted([...discovered].filter((id) => !localized.has(id)));
|
||||
}
|
||||
|
||||
function expectExactResourceCoverage(
|
||||
label: string,
|
||||
localizedIds: Iterable<string>,
|
||||
discoveredIds: Iterable<string>,
|
||||
): void {
|
||||
const missing = findMissingIds(localizedIds, discoveredIds);
|
||||
expect(missing, `${label} should cover every discovered resource`).toEqual([]);
|
||||
}
|
||||
|
||||
async function entriesWithFile(root: string, fileName: string): Promise<string[]> {
|
||||
const entries = await readdir(root, { withFileTypes: true });
|
||||
const ids: string[] = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const filePath = path.join(root, entry.name, fileName);
|
||||
try {
|
||||
if ((await stat(filePath)).isFile()) {
|
||||
ids.push(entry.name);
|
||||
}
|
||||
} catch {
|
||||
// Missing optional registry files are ignored, matching resource discovery.
|
||||
}
|
||||
function invariant(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
return sorted(ids);
|
||||
}
|
||||
|
||||
// As of the skills/design-templates split (specs/current/
|
||||
// skills-and-design-templates.md, Phase 0), the SKILL.md catalogue lives
|
||||
// under two sibling roots: `skills/` for functional skills the agent
|
||||
// invokes mid-task, and `design-templates/` for the rendering catalogue.
|
||||
// Both roots feed the same id-keyed `skillCopy` map in
|
||||
// apps/web/src/i18n/content.ts because the runtime looks up localized
|
||||
// copy by id without caring about origin (e.g. ExamplesTab passes
|
||||
// `designTemplates` into `localizeSkillDescription`). The coverage test
|
||||
// therefore validates the union of both roots — that's what the
|
||||
// localized content claims to cover.
|
||||
async function readSkillRootIds(rootName: 'skills' | 'design-templates'): Promise<string[]> {
|
||||
const root = path.join(repoRoot, rootName);
|
||||
const dirs = await entriesWithFile(root, 'SKILL.md');
|
||||
const ids = await Promise.all(
|
||||
dirs.map(async (dir) => {
|
||||
const raw = await readFile(path.join(root, dir, 'SKILL.md'), 'utf8');
|
||||
return readFrontmatterName(raw) ?? dir;
|
||||
}),
|
||||
);
|
||||
return sorted(ids);
|
||||
function assertResourceId(id: string, label: string): void {
|
||||
invariant(RESOURCE_ID_PATTERN.test(id), `${label} has malformed resource id: ${id}`);
|
||||
}
|
||||
|
||||
async function readSkillIds(): Promise<string[]> {
|
||||
const [skills, designTemplates] = await Promise.all([
|
||||
readSkillRootIds('skills'),
|
||||
readSkillRootIds('design-templates'),
|
||||
]);
|
||||
return sorted(new Set([...skills, ...designTemplates]));
|
||||
}
|
||||
|
||||
async function readDesignSystemIds(): Promise<string[]> {
|
||||
return entriesWithFile(path.join(repoRoot, 'design-systems'), 'DESIGN.md');
|
||||
}
|
||||
|
||||
async function readDesignSystemCategories(): Promise<string[]> {
|
||||
const systemsRoot = path.join(repoRoot, 'design-systems');
|
||||
const ids = await readDesignSystemIds();
|
||||
const categories = await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
const raw = await readFile(path.join(systemsRoot, id, 'DESIGN.md'), 'utf8');
|
||||
return /^>\s*Category:\s*(.+?)\s*$/im.exec(raw)?.[1] ?? 'Uncategorized';
|
||||
}),
|
||||
);
|
||||
return sorted(new Set(categories));
|
||||
}
|
||||
|
||||
async function readPromptTemplateSummaries(): Promise<
|
||||
Array<{ id: string; category: string; tags: string[] }>
|
||||
> {
|
||||
const templatesRoot = path.join(repoRoot, 'prompt-templates');
|
||||
const summaries: Array<{ id: string; category: string; tags: string[] }> = [];
|
||||
for (const surface of ['image', 'video']) {
|
||||
const dir = path.join(templatesRoot, surface);
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
|
||||
const raw = JSON.parse(await readFile(path.join(dir, entry.name), 'utf8')) as {
|
||||
id?: unknown;
|
||||
category?: unknown;
|
||||
tags?: unknown;
|
||||
};
|
||||
if (typeof raw.id !== 'string' || !raw.id) continue;
|
||||
summaries.push({
|
||||
id: raw.id,
|
||||
category: typeof raw.category === 'string' ? raw.category : 'General',
|
||||
tags: Array.isArray(raw.tags) ? raw.tags.filter((tag): tag is string => typeof tag === 'string') : [],
|
||||
});
|
||||
}
|
||||
async function assertDirectory(root: string, label: string): Promise<void> {
|
||||
let info;
|
||||
try {
|
||||
info = await stat(root);
|
||||
} catch (error) {
|
||||
throw new Error(`${label} root is missing: ${root}`, { cause: error });
|
||||
}
|
||||
return summaries;
|
||||
invariant(info.isDirectory(), `${label} root is not a directory: ${root}`);
|
||||
}
|
||||
|
||||
function readFrontmatterName(src: string): string | null {
|
||||
function normalizeText(value: string): string {
|
||||
return value.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function extractYamlScalar(frontmatter: string, key: string): string | null {
|
||||
const lines = frontmatter.split(/\r?\n/);
|
||||
const keyPattern = new RegExp(`^${key}:\\s*(.*?)\\s*$`);
|
||||
const keyIndex = lines.findIndex((line) => keyPattern.test(line));
|
||||
if (keyIndex === -1) return null;
|
||||
|
||||
const keyLine = lines[keyIndex];
|
||||
invariant(keyLine, `YAML key ${key} is missing after lookup`);
|
||||
const rawValue = keyPattern.exec(keyLine)?.[1]?.trim() ?? '';
|
||||
invariant(
|
||||
!rawValue.startsWith('|') || /^([|])[-+]?$/.test(rawValue),
|
||||
`Skill frontmatter key ${key} has malformed block scalar marker: ${rawValue}`,
|
||||
);
|
||||
invariant(
|
||||
!rawValue.startsWith('>') || /^([>])[-+]?$/.test(rawValue),
|
||||
`Skill frontmatter key ${key} has malformed block scalar marker: ${rawValue}`,
|
||||
);
|
||||
const blockMarker = /^([|>])[-+]?$/.exec(rawValue)?.[1];
|
||||
if (blockMarker) {
|
||||
const blockLines: string[] = [];
|
||||
for (const line of lines.slice(keyIndex + 1)) {
|
||||
if (/^\S/.test(line)) break;
|
||||
blockLines.push(line.replace(/^\s{2}/, ''));
|
||||
}
|
||||
const value = normalizeText(blockLines.join(blockMarker === '>' ? ' ' : '\n'));
|
||||
return value || null;
|
||||
}
|
||||
|
||||
invariant(
|
||||
!rawValue.startsWith('"') || rawValue.endsWith('"'),
|
||||
`Skill frontmatter key ${key} has malformed quoted scalar`,
|
||||
);
|
||||
invariant(
|
||||
!rawValue.startsWith("'") || rawValue.endsWith("'"),
|
||||
`Skill frontmatter key ${key} has malformed quoted scalar`,
|
||||
);
|
||||
invariant(
|
||||
!rawValue.endsWith('"') || rawValue.startsWith('"'),
|
||||
`Skill frontmatter key ${key} has malformed quoted scalar`,
|
||||
);
|
||||
invariant(
|
||||
!rawValue.endsWith("'") || rawValue.startsWith("'"),
|
||||
`Skill frontmatter key ${key} has malformed quoted scalar`,
|
||||
);
|
||||
|
||||
const value = unquoteYamlScalar(rawValue);
|
||||
return value ? normalizeText(value) : null;
|
||||
}
|
||||
|
||||
function parseFrontmatter(filePath: string, src: string): string {
|
||||
const text = src.replace(/^\uFEFF/, '');
|
||||
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
|
||||
if (match == null) return null;
|
||||
const nameMatch = /^name:\s*(.*?)\s*$/im.exec(match[1] ?? '');
|
||||
if (nameMatch == null) return null;
|
||||
const name = unquoteYamlScalar(nameMatch[1] ?? '').trim();
|
||||
return name || null;
|
||||
invariant(match?.[1], `Skill frontmatter is missing: ${filePath}`);
|
||||
return match[1];
|
||||
}
|
||||
|
||||
function unquoteYamlScalar(value: string): string {
|
||||
|
|
@ -169,49 +152,255 @@ function unquoteYamlScalar(value: string): string {
|
|||
return trimmed;
|
||||
}
|
||||
|
||||
async function readSkillRootResources(rootName: 'skills' | 'design-templates'): Promise<SkillResource[]> {
|
||||
const skillsRoot = path.join(repoRoot, rootName);
|
||||
await assertDirectory(skillsRoot, rootName);
|
||||
|
||||
const entries = await readdir(skillsRoot, { withFileTypes: true });
|
||||
const resources = await Promise.all(
|
||||
entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map(async (entry) => {
|
||||
const filePath = path.join(skillsRoot, entry.name, 'SKILL.md');
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await readFile(filePath, 'utf8');
|
||||
} catch (error) {
|
||||
throw new Error(`${rootName} resource is missing required file: ${filePath}`, { cause: error });
|
||||
}
|
||||
const frontmatter = parseFrontmatter(filePath, raw);
|
||||
const id = extractYamlScalar(frontmatter, 'name') ?? entry.name;
|
||||
assertResourceId(id, `${rootName} ${entry.name}`);
|
||||
const description = extractYamlScalar(frontmatter, 'description');
|
||||
invariant(
|
||||
description,
|
||||
`${rootName} ${id} is missing required English fallback field: description`,
|
||||
);
|
||||
return { id, description };
|
||||
}),
|
||||
);
|
||||
|
||||
return resources.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
async function readSkillResources(): Promise<SkillResource[]> {
|
||||
const [skills, designTemplates] = await Promise.all([
|
||||
readSkillRootResources('skills'),
|
||||
readSkillRootResources('design-templates'),
|
||||
]);
|
||||
return [...skills, ...designTemplates].sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
async function readDesignSystemResources(): Promise<DesignSystemResource[]> {
|
||||
const systemsRoot = path.join(repoRoot, 'design-systems');
|
||||
await assertDirectory(systemsRoot, 'design systems');
|
||||
|
||||
const entries = await readdir(systemsRoot, { withFileTypes: true });
|
||||
const resources = await Promise.all(
|
||||
entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map(async (entry) => {
|
||||
assertResourceId(entry.name, `Design system directory ${entry.name}`);
|
||||
const filePath = path.join(systemsRoot, entry.name, 'DESIGN.md');
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await readFile(filePath, 'utf8');
|
||||
} catch (error) {
|
||||
throw new Error(`Design system resource is missing required file: ${filePath}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
const category = normalizeText(/^>\s*Category:\s*(.+?)\s*$/im.exec(raw)?.[1] ?? '');
|
||||
invariant(
|
||||
category,
|
||||
`Design system ${entry.name} is missing required English fallback field: category`,
|
||||
);
|
||||
|
||||
const summaryLine = raw
|
||||
.split(/\r?\n/)
|
||||
.find((line) => /^>\s*(?!Category:)(.+?)\s*$/i.test(line));
|
||||
const summary = summaryLine ? normalizeText(summaryLine.replace(/^>\s*/, '')) : null;
|
||||
|
||||
invariant(
|
||||
summary || category,
|
||||
`Design system ${entry.name} is missing required English fallback field: summary or category fallback`,
|
||||
);
|
||||
|
||||
return { id: entry.name, category, summary };
|
||||
}),
|
||||
);
|
||||
|
||||
return resources.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
async function readPromptTemplateResources(): Promise<PromptTemplateResource[]> {
|
||||
const templatesRoot = path.join(repoRoot, 'prompt-templates');
|
||||
await assertDirectory(templatesRoot, 'prompt templates');
|
||||
|
||||
const resources: PromptTemplateResource[] = [];
|
||||
for (const surface of ['image', 'video']) {
|
||||
const dir = path.join(templatesRoot, surface);
|
||||
await assertDirectory(dir, `prompt templates/${surface}`);
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
|
||||
const filePath = path.join(dir, entry.name);
|
||||
let rawText: string;
|
||||
try {
|
||||
rawText = await readFile(filePath, 'utf8');
|
||||
} catch (error) {
|
||||
throw new Error(`Prompt template resource is unreadable: ${filePath}`, { cause: error });
|
||||
}
|
||||
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = JSON.parse(rawText);
|
||||
} catch (error) {
|
||||
throw new Error(`Prompt template JSON is malformed: ${filePath}`, { cause: error });
|
||||
}
|
||||
|
||||
invariant(
|
||||
Boolean(raw) && typeof raw === 'object' && !Array.isArray(raw),
|
||||
`Prompt template ${filePath} must be a JSON object`,
|
||||
);
|
||||
|
||||
const template = raw as Record<string, unknown>;
|
||||
|
||||
invariant(
|
||||
typeof template.id === 'string' && template.id.trim().length > 0,
|
||||
`Prompt template ${filePath} is missing or has malformed required id`,
|
||||
);
|
||||
const id = template.id.trim();
|
||||
assertResourceId(id, `Prompt template ${filePath}`);
|
||||
invariant(
|
||||
template.surface === surface,
|
||||
`Prompt template ${id} has mismatched surface metadata: expected ${surface}`,
|
||||
);
|
||||
invariant(
|
||||
typeof template.title === 'string' && template.title.trim().length > 0,
|
||||
`Prompt template ${id} is missing required English fallback field: title`,
|
||||
);
|
||||
invariant(
|
||||
typeof template.prompt === 'string' && template.prompt.trim().length >= 20,
|
||||
`Prompt template ${id} is missing or has malformed required prompt`,
|
||||
);
|
||||
|
||||
const source = template.source;
|
||||
invariant(
|
||||
Boolean(source) && typeof source === 'object' && !Array.isArray(source),
|
||||
`Prompt template ${id} is missing or has malformed source metadata`,
|
||||
);
|
||||
const sourceRecord = source as Record<string, unknown>;
|
||||
invariant(
|
||||
typeof sourceRecord.repo === 'string' && typeof sourceRecord.license === 'string',
|
||||
`Prompt template ${id} is missing source.repo or source.license`,
|
||||
);
|
||||
|
||||
const summary = typeof template.summary === 'string' ? normalizeText(template.summary) : '';
|
||||
invariant(
|
||||
summary,
|
||||
`Prompt template ${id} is missing required English fallback field: summary`,
|
||||
);
|
||||
const category =
|
||||
typeof template.category === 'string' ? normalizeText(template.category) || 'General' : 'General';
|
||||
const tags = Array.isArray(template.tags)
|
||||
? template.tags
|
||||
.filter((tag): tag is string => typeof tag === 'string')
|
||||
.map((tag) => normalizeText(tag))
|
||||
.filter((tag) => tag.length > 0)
|
||||
: [];
|
||||
|
||||
resources.push({
|
||||
id,
|
||||
title: normalizeText(template.title),
|
||||
summary,
|
||||
category,
|
||||
tags,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return resources.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
describe('localized display content coverage', () => {
|
||||
for (const [locale, ids] of Object.entries(LOCALIZED_CONTENT_IDS)) {
|
||||
it(`covers every curated skill, design system, and prompt template for ${locale}`, async () => {
|
||||
const [skillIds, designSystemIds, promptTemplateSummaries] = await Promise.all([
|
||||
readSkillIds(),
|
||||
readDesignSystemIds(),
|
||||
readPromptTemplateSummaries(),
|
||||
it('derives displayable resources from discovered English fallback content', async () => {
|
||||
const [skills, designSystems, promptTemplates] = await Promise.all([
|
||||
readSkillResources(),
|
||||
readDesignSystemResources(),
|
||||
readPromptTemplateResources(),
|
||||
]);
|
||||
|
||||
expect(uniqueSorted(skills.map((skill) => skill.id)), 'Expected discovered skills to be readable').not.toEqual([]);
|
||||
expect(
|
||||
uniqueSorted(designSystems.map((system) => system.id)),
|
||||
'Expected discovered design systems to be readable',
|
||||
).not.toEqual([]);
|
||||
expect(
|
||||
uniqueSorted(promptTemplates.map((template) => template.id)),
|
||||
'Expected discovered prompt templates to be readable',
|
||||
).not.toEqual([]);
|
||||
|
||||
for (const locale of COVERAGE_LOCALES) {
|
||||
for (const skill of skills) {
|
||||
expect(
|
||||
normalizeText(localizeSkillDescription(locale, skill)),
|
||||
`${locale} should display a skill description for ${skill.id}`,
|
||||
).not.toEqual('');
|
||||
}
|
||||
|
||||
for (const system of designSystems) {
|
||||
expect(
|
||||
normalizeText(localizeDesignSystemSummary(locale, system)),
|
||||
`${locale} should display a design-system summary for ${system.id}`,
|
||||
).not.toEqual('');
|
||||
}
|
||||
|
||||
for (const template of promptTemplates) {
|
||||
const localized = localizePromptTemplateSummary(locale, template);
|
||||
expect(
|
||||
normalizeText(localized.title),
|
||||
`${locale} should display a prompt-template title for ${template.id}`,
|
||||
).not.toEqual('');
|
||||
expect(
|
||||
normalizeText(localized.summary),
|
||||
`${locale} should display a prompt-template summary for ${template.id}`,
|
||||
).not.toEqual('');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const locale of COVERAGE_LOCALES) {
|
||||
const ids = LOCALIZED_CONTENT_IDS[locale];
|
||||
invariant(ids, `Localized content ids are missing for ${locale}`);
|
||||
|
||||
it(`covers every discovered design-system category and prompt tag for ${locale}`, async () => {
|
||||
const [designSystems, promptTemplates] = await Promise.all([
|
||||
readDesignSystemResources(),
|
||||
readPromptTemplateResources(),
|
||||
]);
|
||||
|
||||
expectExactResourceCoverage('skills display copy', ids.skills, skillIds);
|
||||
expectExactResourceCoverage(
|
||||
'design-system summaries',
|
||||
ids.designSystems,
|
||||
designSystemIds,
|
||||
const designSystemCategories = uniqueSorted(designSystems.map((system) => system.category));
|
||||
const promptTemplateCategories = uniqueSorted(
|
||||
promptTemplates.map((template) => template.category),
|
||||
);
|
||||
expectExactResourceCoverage(
|
||||
'prompt-template metadata',
|
||||
ids.promptTemplates,
|
||||
uniqueSorted(promptTemplateSummaries.map((template) => template.id)),
|
||||
);
|
||||
});
|
||||
|
||||
it(`covers every curated display category and prompt tag for ${locale}`, async () => {
|
||||
const [designSystemCategories, promptTemplateSummaries] = await Promise.all([
|
||||
readDesignSystemCategories(),
|
||||
readPromptTemplateSummaries(),
|
||||
]);
|
||||
const promptTemplateCategories = new Set(
|
||||
promptTemplateSummaries.map((template) => template.category),
|
||||
);
|
||||
const promptTemplateTags = new Set(
|
||||
promptTemplateSummaries.flatMap((template) => template.tags),
|
||||
const promptTemplateTags = uniqueSorted(
|
||||
promptTemplates.flatMap((template) => template.tags),
|
||||
);
|
||||
|
||||
expect(sorted(ids.designSystemCategories)).toEqual(
|
||||
expect.arrayContaining(designSystemCategories),
|
||||
);
|
||||
expect(sorted(ids.promptTemplateCategories)).toEqual(
|
||||
expect.arrayContaining(sorted(promptTemplateCategories)),
|
||||
);
|
||||
expect(sorted(ids.promptTemplateTags)).toEqual(
|
||||
expect.arrayContaining(sorted(promptTemplateTags)),
|
||||
);
|
||||
expect(
|
||||
sorted(ids.designSystemCategories),
|
||||
`${locale} is missing localized design-system category translations for: ${designSystemCategories.filter((category) => !ids.designSystemCategories.includes(category)).join(', ') || 'none'}`,
|
||||
).toEqual(expect.arrayContaining(designSystemCategories));
|
||||
expect(
|
||||
sorted(ids.promptTemplateCategories),
|
||||
`${locale} is missing localized prompt-template category translations for: ${promptTemplateCategories.filter((category) => !ids.promptTemplateCategories.includes(category)).join(', ') || 'none'}`,
|
||||
).toEqual(expect.arrayContaining(promptTemplateCategories));
|
||||
expect(
|
||||
sorted(ids.promptTemplateTags),
|
||||
`${locale} is missing localized prompt-template tag translations for: ${promptTemplateTags.filter((tag) => !ids.promptTemplateTags.includes(tag)).join(', ') || 'none'}`,
|
||||
).toEqual(expect.arrayContaining(promptTemplateTags));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,240 @@
|
|||
---
|
||||
id: 20260511-default-english-resource-i18n-fallback
|
||||
name: Default English Resource I18n Fallback
|
||||
status: implemented
|
||||
created: '2026-05-11'
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
### Problem Statement
|
||||
|
||||
- `content.ts`, `content.fr.ts`, and `content.ru.ts` maintain large hand-authored English fallback ID arrays for localized resource display.
|
||||
- Runtime localization already uses English resource fields when localized copy is absent, so the arrays mostly exist to satisfy coverage tests.
|
||||
- Multiple PRs that add skills, design systems, or prompt templates edit the same fallback arrays, causing large merge conflicts.
|
||||
- Moving fallback declarations into `SKILL.md` frontmatter would shift that bookkeeping burden to asset authors and make skill contributions harder.
|
||||
|
||||
### Goals
|
||||
|
||||
- Make English fallback the default runtime behavior for resource display in every locale.
|
||||
- Remove `*_IDS_WITH_EN_FALLBACK` arrays from `content.ts`, `content.fr.ts`, and `content.ru.ts`.
|
||||
- Keep localized copy maps as the only hand-authored resource localization data.
|
||||
- Preserve the existing display behavior: localized copy wins, English source data fills any missing copy.
|
||||
- Keep resource coverage scoped to `de`, `fr`, and `ru`, the current locales with resource display copy dictionaries.
|
||||
- Keep coverage that proves every discovered curated resource has the English source fields needed for fallback display.
|
||||
|
||||
### Scope
|
||||
|
||||
- Update web localized ID construction so it tracks localized copy dictionaries and category/tag dictionaries without fallback arrays.
|
||||
- Remove hand-maintained fallback arrays and their imports/exports.
|
||||
- Update localized coverage tests to validate default fallback semantics from discovered resource source data.
|
||||
- Leave `SKILL.md`, `DESIGN.md`, and prompt-template JSON asset metadata unchanged.
|
||||
|
||||
### Constraints
|
||||
|
||||
- Do not commit a generated registry file.
|
||||
- Avoid moving merge conflicts from centralized content files into asset metadata.
|
||||
- Keep `packages/contracts` unchanged unless runtime API payloads actually change.
|
||||
- Keep resource-localized locale scope explicit: current localized resource content exists for `de`, `fr`, and `ru`.
|
||||
|
||||
### Success Criteria
|
||||
|
||||
- Adding a new English-only skill, design system, or prompt template requires no per-resource fallback ID list edit or per-resource localized copy edit for display fields; category/tag taxonomy localization still follows existing coverage.
|
||||
- Concurrent asset PRs do not need to touch fallback arrays.
|
||||
- Localized display still shows translated copy when present and English resource fields when absent.
|
||||
- Coverage fails when a discovered curated resource lacks required English source fields for fallback display.
|
||||
|
||||
## Research
|
||||
|
||||
### Existing System
|
||||
|
||||
- Supported UI locales include many languages, but resource display localization is only wired for `de`, `ru`, and `fr` through `LOCALIZED_CONTENT_IDS`. Source: `apps/web/src/i18n/types.ts:1-5`; `apps/web/src/i18n/content.ts:1114-1174`
|
||||
- Web i18n display content currently stores localized copy maps plus three fallback ID arrays per resource-localized locale. Source: `apps/web/src/i18n/content.ts:40-49,367-439,542-551`; `apps/web/src/i18n/content.fr.ts:320-393,493-502`; `apps/web/src/i18n/content.ru.ts:320-393,493-502`
|
||||
- `LOCALIZED_CONTENT` wires `de`, `ru`, and `fr` to copy maps and fallback arrays; `buildLocalizedContentIds` merges copy keys with fallback IDs for coverage. Source: `apps/web/src/i18n/content.ts:1114-1174`
|
||||
- Runtime localization already falls back to English source fields when a localized resource copy is missing. Source: `apps/web/src/i18n/content.ts:1188-1232`
|
||||
- Localized coverage is an e2e Vitest test that imports `LOCALIZED_CONTENT_IDS`, discovers skills, design systems, and prompt templates from resource directories, and expects every discovered resource ID to appear in localized IDs. Source: `e2e/tests/localized-content.test.ts:26-37,53-60,154-174`
|
||||
- Skill IDs are discovered by scanning `skills/*/SKILL.md`; the test reads frontmatter `name` when present, then falls back to the directory name. Source: `e2e/tests/localized-content.test.ts:62-89,133-151`
|
||||
- Design system IDs are discovered from `design-systems/*/DESIGN.md`; categories are parsed from a `> Category:` blockquote line. Source: `e2e/tests/localized-content.test.ts:91-104`
|
||||
- Prompt template IDs, categories, and tags are discovered from `prompt-templates/{image,video}/*.json`. Source: `e2e/tests/localized-content.test.ts:107-130`
|
||||
- Daemon resource registries already expose source English fields used by runtime fallback; web registry fetchers consume those daemon endpoints. Source: `apps/daemon/src/skills.ts:94-171`; `apps/daemon/src/design-systems.ts:23-48`; `apps/daemon/src/prompt-templates.ts:36-70`; `apps/web/src/providers/registry.ts:86-94,170-198`
|
||||
- Git history shows fallback arrays grew as a coverage mechanism after resource imports and locale additions: `abaae96e` added design-system coverage for 57 imported systems, `f12471f2` generalized fallback arrays, `10e8e2d3` copied the model for Russian, and `c881c0ca` copied it for French. Source: `git log -S 'WITH_EN_FALLBACK' -- apps/web/src/i18n/content.ts apps/web/src/i18n/content.fr.ts apps/web/src/i18n/content.ru.ts`
|
||||
|
||||
### Available Approaches
|
||||
|
||||
- **Option A: default English fallback with e2e displayability checks**. Remove manual resource fallback IDs, keep localized copy maps, and let coverage derive displayability from discovered source resources plus existing runtime fallback behavior. Source: `apps/web/src/i18n/content.ts:1188-1232`; `e2e/tests/localized-content.test.ts:62-130,154-174`
|
||||
- **Option B: keep centralized fallback arrays**. Current implementation stores explicit fallback arrays per locale and per asset family in `content.ts`, `content.fr.ts`, and `content.ru.ts`. Source: `apps/web/src/i18n/content.ts:367-439,542-551`; `apps/web/src/i18n/content.fr.ts:320-393,493-502`; `apps/web/src/i18n/content.ru.ts:320-393,493-502`
|
||||
- **Option C: asset self-declared fallback metadata**. Asset scanners could read `i18n.fallbackToEnglish` from owner files, but this adds required i18n bookkeeping to every English-only asset contribution. Source: `apps/daemon/src/skills.ts:94-171`; `apps/daemon/src/design-systems.ts:23-48`; `apps/daemon/src/prompt-templates.ts:36-70`
|
||||
- **Option D: generated fallback registry consumed by web**. The repository already has a generated-artifact pattern for artifact manifests, but the i18n coverage test currently computes coverage directly from source content and on-disk assets. Source: `apps/web/src/artifacts/manifest.ts:68-93,96-145`; `apps/web/tests/artifacts/manifest.test.ts:10-57,107-120`; `e2e/tests/localized-content.test.ts:154-174`
|
||||
|
||||
### Constraints & Dependencies
|
||||
|
||||
- `packages/contracts` is the shared web/daemon app contract layer and must stay pure TypeScript; web/daemon DTO changes belong there when API payload shapes change. Source: `packages/AGENTS.md:5-13`; `packages/contracts/src/api/registry.ts:25-83`
|
||||
- App tests live under package/app-level `tests/`; cross-app/resource consistency checks belong in `e2e/tests/`. Source: `AGENTS.md:54-60`; `apps/AGENTS.md:27-32`; `e2e/AGENTS.md:19-38`
|
||||
- Web source code receives resource lists from daemon/runtime summaries, while e2e can scan repository directories directly for cross-resource coverage. Source: `apps/web/src/providers/registry.ts:86-94,170-198`; `e2e/tests/localized-content.test.ts:62-130`
|
||||
- Some current code catches missing resource directories or malformed asset files and returns empty lists or skips entries. E2E discovery for this coverage should fail fast on missing roots, malformed JSON/frontmatter, missing IDs, and missing required English display fields. Source: `apps/daemon/src/skills.ts:94-110`; `apps/daemon/src/design-systems.ts:23-51`; `apps/daemon/src/prompt-templates.ts:36-59`; `e2e/tests/localized-content.test.ts:53-60`
|
||||
- Coverage currently validates the union of localized copy keys and fallback arrays, so removing fallback arrays requires updating the test contract around `LOCALIZED_CONTENT_IDS`. Source: `apps/web/src/i18n/content.ts:1150-1174`; `e2e/tests/localized-content.test.ts:154-174`
|
||||
|
||||
### Key References
|
||||
|
||||
- `apps/web/src/i18n/content.ts:40-49,367-439,542-551,1114-1174` - central localized content bundle, German fallback arrays, localized ID construction.
|
||||
- `apps/web/src/i18n/content.fr.ts:320-393,493-502` - French fallback arrays.
|
||||
- `apps/web/src/i18n/content.ru.ts:320-393,493-502` - Russian fallback arrays.
|
||||
- `apps/web/src/i18n/content.ts:1188-1232` - runtime localization functions and existing English fallback behavior.
|
||||
- `e2e/tests/localized-content.test.ts:26-37,53-60,62-130,154-174` - coverage test and resource discovery logic.
|
||||
- `apps/web/src/i18n/types.ts:1-5` - full UI locale set.
|
||||
- `apps/web/src/providers/registry.ts:86-94,170-198` - runtime resource summaries flow from daemon APIs into web.
|
||||
|
||||
## Design
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Resources[Curated resources\nskills/design systems/templates]
|
||||
Daemon[Daemon registries\nEnglish source summaries]
|
||||
Web[Web registry fetchers]
|
||||
Content[content*.ts\nlocalized copy maps]
|
||||
Runtime[Localization functions\nlocalized copy > English source]
|
||||
Coverage[e2e localized-content test\nresource discovery + displayability]
|
||||
|
||||
Resources --> Daemon
|
||||
Daemon --> Web
|
||||
Web --> Runtime
|
||||
Content --> Runtime
|
||||
Resources --> Coverage
|
||||
Content --> Coverage
|
||||
Coverage --> Runtime
|
||||
```
|
||||
|
||||
Recommended architecture: default English fallback, web-owned resource-localization semantics, and e2e-owned cross-resource coverage.
|
||||
|
||||
### Change Scope
|
||||
|
||||
- Area: asset metadata. Impact: leave `SKILL.md`, `DESIGN.md`, and prompt-template JSON unchanged; no `i18n.fallbackToEnglish` declarations. Source: `skills/dcf-valuation/SKILL.md:1-26`; `design-systems/wechat/DESIGN.md:1-5`; `prompt-templates/image/social-media-post-showa-day-retro-culture-magazine-cover.json:1-22`
|
||||
- Area: contracts and daemon APIs. Impact: no API DTO changes for fallback; existing English summaries remain the fallback source. Source: `packages/contracts/src/api/registry.ts:25-83`; `apps/daemon/src/static-resource-routes.ts:46-68,148-176`; `apps/web/src/providers/registry.ts:86-94,170-198`
|
||||
- Area: web i18n. Impact: remove hand-authored fallback arrays and keep localized copy/category/tag maps; runtime localization keeps localized-copy-first English fallback. Source: `apps/web/src/i18n/content.ts:40-49,1114-1174,1188-1232`; `apps/web/src/i18n/content.fr.ts:320-393,493-502`; `apps/web/src/i18n/content.ru.ts:320-393,493-502`
|
||||
- Area: localized ID semantics. Impact: keep `LOCALIZED_CONTENT_IDS` focused on localized copy/category/tag dictionaries; move all-resource displayability coverage to e2e discovery. Source: `apps/web/src/i18n/content.ts:1150-1174`; `e2e/tests/localized-content.test.ts:154-174`
|
||||
- Area: coverage. Impact: update `e2e/tests/localized-content.test.ts` to discover resources, fail fast on malformed resource sources, and assert each resource has required English fallback fields; keep category/tag coverage for `de`, `fr`, and `ru`. Source: `e2e/tests/localized-content.test.ts:53-60,62-130,154-197`; `e2e/AGENTS.md:19-38`
|
||||
|
||||
### Design Decisions
|
||||
|
||||
- Decision: English fallback is implicit at runtime for every locale; coverage for resource-localized dictionaries remains scoped to `de`, `fr`, and `ru`. Source: `apps/web/src/i18n/content.ts:1188-1232`; `apps/web/src/i18n/content.ts:1114-1174`; `e2e/tests/localized-content.test.ts:62-130,154-174`
|
||||
- Decision: `de`, `fr`, and `ru` remain the explicit resource-localized locale set because only those locales have resource display copy modules. Source: `apps/web/src/i18n/content.ts:1114-1174`; `apps/web/src/i18n/types.ts:1-5`
|
||||
- Decision: `SKILL.md` frontmatter and other asset metadata stay focused on asset behavior, not fallback bookkeeping. Source: `apps/daemon/src/skills.ts:29-34,94-171`; `skills/dcf-valuation/SKILL.md:1-26`
|
||||
- Decision: keep web i18n as the owner of fallback semantics because localized copy maps and runtime display functions live there. Source: `apps/web/src/i18n/content.ts:40-49,1114-1174,1188-1232`
|
||||
- Decision: keep cross-resource coverage in e2e because it spans apps/web i18n source and top-level resource directories. Source: `AGENTS.md:54-60`; `e2e/AGENTS.md:19-38`; `e2e/tests/localized-content.test.ts:26-37,62-130`
|
||||
- Decision: avoid generated fallback registries; e2e discovery and runtime daemon summaries already provide resource lists in their respective environments. Source: `e2e/tests/localized-content.test.ts:62-130`; `apps/web/src/providers/registry.ts:86-94,170-198`
|
||||
|
||||
### Why this design
|
||||
|
||||
- It removes merge conflict hotspots without adding i18n chores to every asset contribution.
|
||||
- It matches current runtime behavior: localized copy overrides English source data, and missing localized copy displays English.
|
||||
- It keeps locale semantics centralized in web i18n.
|
||||
- It preserves coverage for displayability while turning missing translation into translation debt instead of contributor-blocking metadata.
|
||||
|
||||
### Test Strategy
|
||||
|
||||
- Web i18n: add or update tests for exported localized IDs to prove fallback arrays are gone and localized copy/category/tag dictionaries still drive localized metadata coverage. Source: `apps/AGENTS.md:27-32`; `apps/web/src/i18n/content.ts:1150-1174`
|
||||
- E2E: update `e2e/tests/localized-content.test.ts` so source resource displayability derives from discovered resources plus default English fallback. Required fields: skill description, design-system summary/category fallback input, prompt-template title and summary; skill `examplePrompt` remains optional. Source: `e2e/tests/localized-content.test.ts:62-130,154-197`; `e2e/AGENTS.md:19-38`
|
||||
- E2E: keep category and prompt tag coverage against localized dictionaries because category/tag strings do not come from a single resource summary fallback path. Source: `e2e/tests/localized-content.test.ts:176-197`
|
||||
- Validation commands: run package-scoped checks for changed packages plus repo-level guard/typecheck. Source: `AGENTS.md:87-108`; `apps/AGENTS.md:47-59`; `packages/AGENTS.md:22-36`; `e2e/AGENTS.md:40-55`
|
||||
|
||||
### Pseudocode
|
||||
|
||||
```ts
|
||||
const RESOURCE_LOCALIZED_LOCALES = ['de', 'fr', 'ru'] as const;
|
||||
|
||||
function buildLocalizedContentIds(content) {
|
||||
return {
|
||||
skills: Object.keys(content.skillCopy),
|
||||
designSystems: Object.keys(content.designSystemSummaries),
|
||||
promptTemplates: Object.keys(content.promptTemplateCopy),
|
||||
designSystemCategories: Object.keys(content.designSystemCategories),
|
||||
promptTemplateCategories: Object.keys(content.promptTemplateCategories),
|
||||
promptTemplateTags: Object.keys(content.promptTemplateTags),
|
||||
};
|
||||
}
|
||||
|
||||
function expectResourceDisplayable(locale, resource) {
|
||||
const localized = localizeResource(locale, resource);
|
||||
expect(requiredDisplayFields(localized)).toBeNonEmpty();
|
||||
}
|
||||
|
||||
// Required English fallback fields:
|
||||
// - skills: description; examplePrompt is optional
|
||||
// - design systems: summary or category
|
||||
// - prompt templates: title and summary
|
||||
|
||||
function discoverResourcesOrThrow() {
|
||||
// Fail on missing resource roots, malformed JSON/frontmatter,
|
||||
// missing IDs, and missing required English fallback fields.
|
||||
}
|
||||
```
|
||||
|
||||
### File Structure
|
||||
|
||||
- `apps/web/src/i18n/content.ts` - remove hand-maintained fallback arrays and keep localized-copy-first runtime fallback behavior.
|
||||
- `apps/web/src/i18n/content.fr.ts` - remove exported fallback arrays.
|
||||
- `apps/web/src/i18n/content.ru.ts` - remove exported fallback arrays.
|
||||
- `apps/web/tests/i18n/content.test.ts` or existing i18n tests - localized ID and fallback behavior coverage.
|
||||
- `e2e/tests/localized-content.test.ts` - default fallback displayability coverage.
|
||||
|
||||
### Interfaces / APIs
|
||||
|
||||
```ts
|
||||
type ResourceLocalizedLocale = Extract<Locale, 'de' | 'fr' | 'ru'>;
|
||||
|
||||
type LocalizedContentIds = {
|
||||
skills: string[];
|
||||
designSystems: string[];
|
||||
designSystemCategories: string[];
|
||||
promptTemplates: string[];
|
||||
promptTemplateCategories: string[];
|
||||
promptTemplateTags: string[];
|
||||
};
|
||||
```
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Resource appears in localized copy map but is missing from discovered resources: keep it in localized IDs and let coverage/reporting surface stale copy if useful.
|
||||
- Resource has partial localized copy, such as only `examplePrompt`: localized field wins field-by-field and English fills missing fields.
|
||||
- New locale with UI dictionary only: runtime resource display uses English fallback; category/tag resource-localization coverage starts when that locale gets resource copy dictionaries.
|
||||
- Category/tag localization remains dictionary-based; missing category/tag entries should remain visible in coverage because the gallery cannot infer translated labels from resource summaries.
|
||||
- Derived skill IDs follow existing discovery behavior and runtime source summaries; no fallback metadata inheritance is needed.
|
||||
|
||||
## Plan
|
||||
|
||||
- [x] Step 1: Remove manual fallback arrays from web i18n
|
||||
- [x] Substep 1.1 Implement: Remove fallback array fields from `LocalizedContentBundle` in `apps/web/src/i18n/content.ts`.
|
||||
- [x] Substep 1.2 Implement: Remove `DE_*_IDS_WITH_EN_FALLBACK`, `FR_*_IDS_WITH_EN_FALLBACK`, and `RU_*_IDS_WITH_EN_FALLBACK` definitions/imports/exports.
|
||||
- [x] Substep 1.3 Implement: Keep localized-copy-first English fallback in `localizeSkill*`, `localizeDesignSystemSummary`, and `localizePromptTemplateSummary`.
|
||||
- [x] Substep 1.4 Verify: Add or update web i18n tests for localized copy precedence and English field fallback.
|
||||
- [x] Step 2: Update localized resource coverage semantics
|
||||
- [x] Substep 2.1 Implement: Update `e2e/tests/localized-content.test.ts` to discover skills, design systems, and prompt templates with fail-fast parsing.
|
||||
- [x] Substep 2.2 Implement: Assert required English fallback fields exist: skill description, design-system summary/category fallback input, prompt-template title and summary.
|
||||
- [x] Substep 2.3 Implement: Keep category and tag coverage against localized dictionaries for `de`, `fr`, and `ru`.
|
||||
- [x] Substep 2.4 Implement: Add clear assertion messages that distinguish missing English fallback fields from missing category/tag translations.
|
||||
- [x] Substep 2.5 Verify: Run the localized-content e2e test file.
|
||||
- [x] Step 3: Clean up docs and validate
|
||||
- [x] Substep 3.1 Implement: Remove comments that describe fallback arrays as required bookkeeping.
|
||||
- [x] Substep 3.2 Verify: Run affected web/e2e typechecks and tests.
|
||||
- [x] Substep 3.3 Verify: Run `pnpm guard` and `pnpm typecheck`.
|
||||
|
||||
## Notes
|
||||
|
||||
### Implementation
|
||||
|
||||
- `apps/web/src/i18n/content.ts` - removed fallback ID fields and arrays; `LOCALIZED_CONTENT_IDS` now derives IDs from localized copy/category/tag dictionaries only.
|
||||
- `apps/web/src/i18n/content.fr.ts` - removed French exported fallback ID arrays.
|
||||
- `apps/web/src/i18n/content.ru.ts` - removed Russian exported fallback ID arrays.
|
||||
- `apps/web/tests/i18n/content.test.ts` - added runtime coverage for localized precedence, English fallback, and localized ID derivation.
|
||||
- `e2e/tests/localized-content.test.ts` - moved resource displayability coverage to discovered English resource fields, added fail-fast resource parsing, and kept localized category/tag coverage for `de`, `fr`, and `ru`.
|
||||
|
||||
### Verification
|
||||
|
||||
- `pnpm --filter @open-design/web exec vitest run -c vitest.config.ts tests/i18n/content.test.ts` - passed.
|
||||
- `pnpm typecheck` from `e2e/` - passed.
|
||||
- `pnpm test tests/localized-content.test.ts` from `e2e/` - passed.
|
||||
- Reviewer subagent - no blocking issues after fixes.
|
||||
- `pnpm guard` - passed.
|
||||
- `pnpm typecheck` - passed.
|
||||
Loading…
Reference in a new issue