feat(examples): translate ExamplesPage + ExamplesGallery to 9 locales

The /examples gallery is fully localised:
- Header (heading + subtitle).
- Search input placeholder + aria-label + the clear button.
- Match-count tag with i18next pluralisation (handles _one /
  _other and Russian's _few / _many).
- Category and Difficulty filter labels + their button labels
  (basics / sensors / displays / communication / games / robotics
  / circuits; beginner / intermediate / advanced).
- Per-card "Copy shareable link" tooltip.
- Empty-state copy with two variants (with-search / without-
  search) interpolating the search query.
- Reset-filters button.
- The library-install progress overlay copy from
  ExamplesPage.tsx ("Installing libraries (N/M)") with done/total
  interpolation.

Internal /editor link uses localize() so a Spanish reader who
clicks an example lands on /es/editor.

Hand-translated for all 8 non-English locales. Per-example titles
+ descriptions are NOT i18n yet — they live in the
src/data/examples* tables and would need a separate pipeline.
DocsPage (2715 lines of prose) deferred too — best handled by
running scripts/translate-i18n.mjs once the keys are extracted.
This commit is contained in:
David Montero Crespo 2026-05-09 11:24:52 -03:00
parent b086be73fe
commit 4df81a3eda
11 changed files with 342 additions and 19 deletions

View File

@ -5,6 +5,7 @@
*/
import React, { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { exampleProjects, type ExampleProject } from '../../data/examples';
import { ExampleThumbnail } from './ExampleThumbnail';
import './ExamplesGallery.css';
@ -44,6 +45,7 @@ function getBoardFilter(example: ExampleProject): string {
}
export const ExamplesGallery: React.FC<ExamplesGalleryProps> = ({ onLoadExample }) => {
const { t } = useTranslation();
const [selectedBoard, setSelectedBoard] = useState<string>('all');
const [selectedCategory, setSelectedCategory] = useState<ExampleProject['category'] | 'all'>(
'all',
@ -193,8 +195,8 @@ export const ExamplesGallery: React.FC<ExamplesGalleryProps> = ({ onLoadExample
return (
<div className="examples-gallery">
<div className="examples-header">
<h1>Featured Projects</h1>
<p>Explore and run example projects organized by board</p>
<h1>{t('examples.heading')}</h1>
<p>{t('examples.subtitle')}</p>
</div>
{/* Search */}
@ -218,18 +220,18 @@ export const ExamplesGallery: React.FC<ExamplesGalleryProps> = ({ onLoadExample
<input
type="search"
className="examples-search-input"
placeholder="Search examples — try 'esp32 oled', 'blink', 'dht'…"
placeholder={t('examples.searchPlaceholder')}
value={search}
onChange={(e) => setSearch(e.target.value)}
aria-label="Search examples"
aria-label={t('examples.searchLabel')}
/>
{search && (
<button
type="button"
className="examples-search-clear"
onClick={() => setSearch('')}
aria-label="Clear search"
title="Clear"
aria-label={t('examples.clearSearch')}
title={t('examples.clear')}
>
×
</button>
@ -237,8 +239,7 @@ export const ExamplesGallery: React.FC<ExamplesGalleryProps> = ({ onLoadExample
</div>
{searchTokens.length > 0 && (
<span className="examples-search-count">
{filteredExamples.length} match
{filteredExamples.length === 1 ? '' : 'es'}
{t('examples.matchCount', { count: filteredExamples.length })}
</span>
)}
</div>
@ -267,7 +268,7 @@ export const ExamplesGallery: React.FC<ExamplesGalleryProps> = ({ onLoadExample
{/* Category + Difficulty filters */}
<div className="examples-filters">
<div className="filter-group">
<label>Category:</label>
<label>{t('examples.filters.categoryLabel')}</label>
<div className="filter-buttons">
{(
[
@ -287,14 +288,14 @@ export const ExamplesGallery: React.FC<ExamplesGalleryProps> = ({ onLoadExample
onClick={() => setSelectedCategory(cat)}
>
{cat !== 'all' && getCategoryIcon(cat as ExampleProject['category'])}{' '}
{cat === 'all' ? 'All' : cat.charAt(0).toUpperCase() + cat.slice(1)}
{t(`examples.filters.category.${cat}`)}
</button>
))}
</div>
</div>
<div className="filter-group">
<label>Difficulty:</label>
<label>{t('examples.filters.difficultyLabel')}</label>
<div className="filter-buttons">
{(['all', 'beginner', 'intermediate', 'advanced'] as const).map((diff) => (
<button
@ -302,7 +303,7 @@ export const ExamplesGallery: React.FC<ExamplesGalleryProps> = ({ onLoadExample
className={`filter-button ${selectedDifficulty === diff ? 'active' : ''}`}
onClick={() => setSelectedDifficulty(diff)}
>
{diff === 'all' ? 'All' : diff.charAt(0).toUpperCase() + diff.slice(1)}
{t(`examples.filters.difficulty.${diff}`)}
</button>
))}
</div>
@ -356,7 +357,7 @@ export const ExamplesGallery: React.FC<ExamplesGalleryProps> = ({ onLoadExample
<button
className="example-copy-link"
onClick={(e) => handleCopyLink(e, example.id)}
title="Copy shareable link"
title={t('examples.copyLink')}
>
{copiedId === example.id ? (
<svg
@ -397,9 +398,9 @@ export const ExamplesGallery: React.FC<ExamplesGalleryProps> = ({ onLoadExample
{filteredExamples.length === 0 && (
<div className="examples-empty">
<p>
No examples found
{searchTokens.length > 0 ? ` for "${search.trim()}"` : ''} with the
selected filters
{searchTokens.length > 0
? t('examples.emptyForQuery', { query: search.trim() })
: t('examples.empty')}
</p>
{(searchTokens.length > 0 ||
selectedBoard !== 'all' ||
@ -414,7 +415,7 @@ export const ExamplesGallery: React.FC<ExamplesGalleryProps> = ({ onLoadExample
setSelectedDifficulty('all');
}}
>
Reset filters
{t('examples.resetFilters')}
</button>
)}
</div>

View File

@ -77,6 +77,41 @@
"ctaPaypal": "Über PayPal spenden"
}
},
"examples": {
"heading": "Empfohlene Projekte",
"subtitle": "Beispielprojekte erkunden und ausführen — nach Board sortiert",
"searchPlaceholder": "Beispiele suchen — z. B. 'esp32 oled', 'blink', 'dht'…",
"searchLabel": "Beispiele suchen",
"clearSearch": "Suche zurücksetzen",
"clear": "Löschen",
"matchCount_one": "{{count}} Treffer",
"matchCount_other": "{{count}} Treffer",
"filters": {
"categoryLabel": "Kategorie:",
"difficultyLabel": "Schwierigkeit:",
"category": {
"all": "Alle",
"basics": "Grundlagen",
"sensors": "Sensoren",
"displays": "Displays",
"communication": "Kommunikation",
"games": "Spiele",
"robotics": "Robotik",
"circuits": "Schaltungen"
},
"difficulty": {
"all": "Alle",
"beginner": "Anfänger",
"intermediate": "Fortgeschritten",
"advanced": "Profi"
}
},
"copyLink": "Teilbaren Link kopieren",
"empty": "Keine Beispiele mit den gewählten Filtern gefunden",
"emptyForQuery": "Keine Beispiele für „{{query}}\" mit den gewählten Filtern gefunden",
"resetFilters": "Filter zurücksetzen",
"installing": "Bibliotheken werden installiert ({{done}}/{{total}})"
},
"about": {
"hero": {
"title": "Über Velxio",

View File

@ -77,6 +77,41 @@
"ctaPaypal": "Donate via PayPal"
}
},
"examples": {
"heading": "Featured Projects",
"subtitle": "Explore and run example projects — organized by board",
"searchPlaceholder": "Search examples — try 'esp32 oled', 'blink', 'dht'…",
"searchLabel": "Search examples",
"clearSearch": "Clear search",
"clear": "Clear",
"matchCount_one": "{{count}} match",
"matchCount_other": "{{count}} matches",
"filters": {
"categoryLabel": "Category:",
"difficultyLabel": "Difficulty:",
"category": {
"all": "All",
"basics": "Basics",
"sensors": "Sensors",
"displays": "Displays",
"communication": "Communication",
"games": "Games",
"robotics": "Robotics",
"circuits": "Circuits"
},
"difficulty": {
"all": "All",
"beginner": "Beginner",
"intermediate": "Intermediate",
"advanced": "Advanced"
}
},
"copyLink": "Copy shareable link",
"empty": "No examples found with the selected filters",
"emptyForQuery": "No examples found for \"{{query}}\" with the selected filters",
"resetFilters": "Reset filters",
"installing": "Installing libraries ({{done}}/{{total}})"
},
"about": {
"hero": {
"title": "About Velxio",

View File

@ -77,6 +77,41 @@
"ctaPaypal": "Donar con PayPal"
}
},
"examples": {
"heading": "Proyectos destacados",
"subtitle": "Explora y ejecuta proyectos de ejemplo — organizados por placa",
"searchPlaceholder": "Buscar ejemplos — prueba 'esp32 oled', 'blink', 'dht'…",
"searchLabel": "Buscar ejemplos",
"clearSearch": "Limpiar búsqueda",
"clear": "Limpiar",
"matchCount_one": "{{count}} coincidencia",
"matchCount_other": "{{count}} coincidencias",
"filters": {
"categoryLabel": "Categoría:",
"difficultyLabel": "Dificultad:",
"category": {
"all": "Todas",
"basics": "Básicos",
"sensors": "Sensores",
"displays": "Pantallas",
"communication": "Comunicación",
"games": "Juegos",
"robotics": "Robótica",
"circuits": "Circuitos"
},
"difficulty": {
"all": "Todas",
"beginner": "Principiante",
"intermediate": "Intermedio",
"advanced": "Avanzado"
}
},
"copyLink": "Copiar enlace para compartir",
"empty": "No se encontraron ejemplos con los filtros seleccionados",
"emptyForQuery": "No se encontraron ejemplos para \"{{query}}\" con los filtros seleccionados",
"resetFilters": "Restablecer filtros",
"installing": "Instalando librerías ({{done}}/{{total}})"
},
"about": {
"hero": {
"title": "Acerca de Velxio",

View File

@ -77,6 +77,41 @@
"ctaPaypal": "Faire un don via PayPal"
}
},
"examples": {
"heading": "Projets en vedette",
"subtitle": "Explorez et exécutez des projets d'exemple — organisés par carte",
"searchPlaceholder": "Rechercher des exemples — essayez 'esp32 oled', 'blink', 'dht'…",
"searchLabel": "Rechercher des exemples",
"clearSearch": "Effacer la recherche",
"clear": "Effacer",
"matchCount_one": "{{count}} résultat",
"matchCount_other": "{{count}} résultats",
"filters": {
"categoryLabel": "Catégorie :",
"difficultyLabel": "Difficulté :",
"category": {
"all": "Toutes",
"basics": "Bases",
"sensors": "Capteurs",
"displays": "Écrans",
"communication": "Communication",
"games": "Jeux",
"robotics": "Robotique",
"circuits": "Circuits"
},
"difficulty": {
"all": "Toutes",
"beginner": "Débutant",
"intermediate": "Intermédiaire",
"advanced": "Avancé"
}
},
"copyLink": "Copier le lien partageable",
"empty": "Aucun exemple trouvé avec les filtres sélectionnés",
"emptyForQuery": "Aucun exemple trouvé pour « {{query}} » avec les filtres sélectionnés",
"resetFilters": "Réinitialiser les filtres",
"installing": "Installation des bibliothèques ({{done}}/{{total}})"
},
"about": {
"hero": {
"title": "À propos de Velxio",

View File

@ -77,6 +77,41 @@
"ctaPaypal": "Dona con PayPal"
}
},
"examples": {
"heading": "Progetti in evidenza",
"subtitle": "Esplora ed esegui progetti di esempio — organizzati per scheda",
"searchPlaceholder": "Cerca esempi — prova 'esp32 oled', 'blink', 'dht'…",
"searchLabel": "Cerca esempi",
"clearSearch": "Cancella ricerca",
"clear": "Cancella",
"matchCount_one": "{{count}} risultato",
"matchCount_other": "{{count}} risultati",
"filters": {
"categoryLabel": "Categoria:",
"difficultyLabel": "Difficoltà:",
"category": {
"all": "Tutte",
"basics": "Basi",
"sensors": "Sensori",
"displays": "Display",
"communication": "Comunicazione",
"games": "Giochi",
"robotics": "Robotica",
"circuits": "Circuiti"
},
"difficulty": {
"all": "Tutte",
"beginner": "Principiante",
"intermediate": "Intermedio",
"advanced": "Avanzato"
}
},
"copyLink": "Copia link condivisibile",
"empty": "Nessun esempio trovato con i filtri selezionati",
"emptyForQuery": "Nessun esempio trovato per \"{{query}}\" con i filtri selezionati",
"resetFilters": "Reimposta filtri",
"installing": "Installazione librerie ({{done}}/{{total}})"
},
"about": {
"hero": {
"title": "Informazioni su Velxio",

View File

@ -77,6 +77,40 @@
"ctaPaypal": "PayPal で寄付する"
}
},
"examples": {
"heading": "おすすめプロジェクト",
"subtitle": "サンプルプロジェクトを探して実行 — ボード別に整理",
"searchPlaceholder": "サンプルを検索 — 例「esp32 oled」「blink」「dht」…",
"searchLabel": "サンプルを検索",
"clearSearch": "検索をクリア",
"clear": "クリア",
"matchCount_other": "{{count}} 件",
"filters": {
"categoryLabel": "カテゴリ:",
"difficultyLabel": "難易度:",
"category": {
"all": "すべて",
"basics": "基礎",
"sensors": "センサー",
"displays": "ディスプレイ",
"communication": "通信",
"games": "ゲーム",
"robotics": "ロボティクス",
"circuits": "回路"
},
"difficulty": {
"all": "すべて",
"beginner": "初級",
"intermediate": "中級",
"advanced": "上級"
}
},
"copyLink": "共有リンクをコピー",
"empty": "選択したフィルタに一致するサンプルが見つかりません",
"emptyForQuery": "「{{query}}」に一致するサンプルが見つかりません(選択したフィルタ条件で)",
"resetFilters": "フィルタをリセット",
"installing": "ライブラリをインストール中({{done}}/{{total}}"
},
"about": {
"hero": {
"title": "Velxio について",

View File

@ -77,6 +77,41 @@
"ctaPaypal": "Doar via PayPal"
}
},
"examples": {
"heading": "Projetos em destaque",
"subtitle": "Explore e execute projetos de exemplo — organizados por placa",
"searchPlaceholder": "Buscar exemplos — tente 'esp32 oled', 'blink', 'dht'…",
"searchLabel": "Buscar exemplos",
"clearSearch": "Limpar busca",
"clear": "Limpar",
"matchCount_one": "{{count}} resultado",
"matchCount_other": "{{count}} resultados",
"filters": {
"categoryLabel": "Categoria:",
"difficultyLabel": "Dificuldade:",
"category": {
"all": "Todas",
"basics": "Básicos",
"sensors": "Sensores",
"displays": "Displays",
"communication": "Comunicação",
"games": "Jogos",
"robotics": "Robótica",
"circuits": "Circuitos"
},
"difficulty": {
"all": "Todas",
"beginner": "Iniciante",
"intermediate": "Intermediário",
"advanced": "Avançado"
}
},
"copyLink": "Copiar link compartilhável",
"empty": "Nenhum exemplo encontrado com os filtros selecionados",
"emptyForQuery": "Nenhum exemplo encontrado para \"{{query}}\" com os filtros selecionados",
"resetFilters": "Limpar filtros",
"installing": "Instalando bibliotecas ({{done}}/{{total}})"
},
"about": {
"hero": {
"title": "Sobre o Velxio",

View File

@ -77,6 +77,43 @@
"ctaPaypal": "Пожертвовать через PayPal"
}
},
"examples": {
"heading": "Избранные проекты",
"subtitle": "Изучайте и запускайте примеры — сгруппированные по плате",
"searchPlaceholder": "Поиск примеров — попробуйте 'esp32 oled', 'blink', 'dht'…",
"searchLabel": "Поиск примеров",
"clearSearch": "Очистить поиск",
"clear": "Очистить",
"matchCount_one": "{{count}} совпадение",
"matchCount_few": "{{count}} совпадения",
"matchCount_many": "{{count}} совпадений",
"matchCount_other": "{{count}} совпадений",
"filters": {
"categoryLabel": "Категория:",
"difficultyLabel": "Сложность:",
"category": {
"all": "Все",
"basics": "Основы",
"sensors": "Датчики",
"displays": "Дисплеи",
"communication": "Связь",
"games": "Игры",
"robotics": "Робототехника",
"circuits": "Схемы"
},
"difficulty": {
"all": "Все",
"beginner": "Новичок",
"intermediate": "Средний",
"advanced": "Продвинутый"
}
},
"copyLink": "Скопировать ссылку для шаринга",
"empty": "Не найдено примеров с выбранными фильтрами",
"emptyForQuery": "Не найдено примеров по запросу «{{query}}» с выбранными фильтрами",
"resetFilters": "Сбросить фильтры",
"installing": "Установка библиотек ({{done}}/{{total}})"
},
"about": {
"hero": {
"title": "О Velxio",

View File

@ -77,6 +77,40 @@
"ctaPaypal": "通过 PayPal 捐赠"
}
},
"examples": {
"heading": "精选项目",
"subtitle": "浏览并运行示例项目 —— 按开发板分类",
"searchPlaceholder": "搜索示例 —— 试试 'esp32 oled'、'blink'、'dht'…",
"searchLabel": "搜索示例",
"clearSearch": "清除搜索",
"clear": "清除",
"matchCount_other": "{{count}} 个结果",
"filters": {
"categoryLabel": "分类:",
"difficultyLabel": "难度:",
"category": {
"all": "全部",
"basics": "基础",
"sensors": "传感器",
"displays": "显示器",
"communication": "通信",
"games": "游戏",
"robotics": "机器人",
"circuits": "电路"
},
"difficulty": {
"all": "全部",
"beginner": "入门",
"intermediate": "中级",
"advanced": "高级"
}
},
"copyLink": "复制可分享链接",
"empty": "在所选筛选条件下未找到示例",
"emptyForQuery": "在所选筛选条件下未找到与「{{query}}」匹配的示例",
"resetFilters": "重置筛选",
"installing": "正在安装库({{done}}/{{total}}"
},
"about": {
"hero": {
"title": "关于 Velxio",

View File

@ -6,14 +6,18 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ExamplesGallery } from '../components/examples/ExamplesGallery';
import { AppHeader } from '../components/layout/AppHeader';
import { useLocalizedHref } from '../i18n/useLocalizedNavigate';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import { loadExample, type LibraryInstallProgress } from '../utils/loadExample';
import type { ExampleProject } from '../data/examples';
export const ExamplesPage: React.FC = () => {
const { t } = useTranslation();
const localize = useLocalizedHref();
useSEO(getSeoMeta('/examples')!);
const navigate = useNavigate();
@ -21,7 +25,7 @@ export const ExamplesPage: React.FC = () => {
const handleLoadExample = async (example: ExampleProject) => {
await loadExample(example, setInstalling);
navigate('/editor');
navigate(localize('/editor'));
};
return (
@ -61,7 +65,10 @@ export const ExamplesPage: React.FC = () => {
}}
>
<div style={{ fontSize: 14, color: '#ccc', marginBottom: 12 }}>
Installing libraries ({installing.done + 1}/{installing.total})
{t('examples.installing', {
done: installing.done + 1,
total: installing.total,
})}
</div>
<div style={{ fontSize: 16, color: '#00e5ff', fontWeight: 600, marginBottom: 16 }}>
{installing.current}