From 770b0a9261b762cbf686ff78289351d0bd2c11cf Mon Sep 17 00:00:00 2001 From: cyz Date: Thu, 1 Oct 2026 08:49:21 -0300 Subject: [PATCH 1/6] Add partial Brazilian Portuguese localization --- docs/homepage.js | 125 +- docs/index.html | 66 +- docs/language-navigation.js | 14 +- docs/localization.js | 291 +++ docs/step.css | 18 + docs/styles.css | 16 + .../markdown-language-preprocessor.test.js | 33 + docs/theme-toggle.js | 8 +- docs/workshop/step.html | 239 ++- scripts/validate_workshop.py | 4 +- workshop/pt-BR/00-preflight.md | 636 ++++++ workshop/pt-BR/01-first-session.md | 760 ++++++++ workshop/pt-BR/02-streaming.md | 967 +++++++++ workshop/pt-BR/03-local-tool.md | 983 ++++++++++ workshop/pt-BR/04-mcp-safety.md | 1724 +++++++++++++++++ 15 files changed, 5747 insertions(+), 137 deletions(-) create mode 100644 docs/localization.js create mode 100644 workshop/pt-BR/00-preflight.md create mode 100644 workshop/pt-BR/01-first-session.md create mode 100644 workshop/pt-BR/02-streaming.md create mode 100644 workshop/pt-BR/03-local-tool.md create mode 100644 workshop/pt-BR/04-mcp-safety.md diff --git a/docs/homepage.js b/docs/homepage.js index 5f5e40a..8762364 100644 --- a/docs/homepage.js +++ b/docs/homepage.js @@ -2,6 +2,8 @@ 'use strict'; const storageKey = 'copilot-sdk-workshop.language'; + const localeStorageKey = 'copilot-sdk-workshop.locale'; + const localeSelector = document.getElementById('localeSelector'); const picker = document.getElementById('languagePicker'); const languageInputs = [...document.querySelectorAll('input[name="language"]')]; const startLink = document.getElementById('startWorkshopLink'); @@ -16,41 +18,39 @@ const startGuidance = document.getElementById('startGuidance'); let selectedWorkshopId = null; - const workshops = { + const workshopMetadata = { sdlc: { - name: 'Accessibility reviewer', previewTitle: 'accessibility-reviewer', - preview: `URL → Playwright inspection - → WCAG lookup - → structured report - -[tool] playwright-browser_navigate -[tool] accessibility_rule_lookup - -Finding -The name input has no accessible name.`, - guidance: 'Build an SDLC developer tool in a 90-minute core workshop.' }, museum: { - name: 'Museum Exhibit Studio', previewTitle: 'museum-exhibit-studio', - preview: `Approved facts → curator session - → exhibit validation - → visitor-ready copy + } + }; + let locale = WorkshopLocalization.resolveLocale( + window.location.search, + getStoredValue(localeStorageKey) + ); + let messages = WorkshopLocalization.getMessages(locale.id); -Available tools: [] -System message: replace + function getStoredValue(key) { + try { + return window.localStorage.getItem(key); + } catch (error) { + return null; + } + } -# Journey to the Moon -## Narrative -## Visitor questions`, - guidance: 'Build a non-SDLC curator tool in a 90-minute core workshop.' + function storeValue(key, value) { + try { + window.localStorage.setItem(key, value); + } catch (error) { + // Local storage can be unavailable in private browsing contexts. } - }; + } function getStoredLanguageId() { try { - return window.localStorage.getItem(storageKey); + return getStoredValue(storageKey); } catch (error) { return null; } @@ -58,16 +58,37 @@ System message: replace function storeLanguageId(languageId) { try { - window.localStorage.setItem(storageKey, languageId); + storeValue(storageKey, languageId); } catch (error) { // Local storage can be unavailable in private browsing contexts. } } + function applyLocale() { + messages = WorkshopLocalization.getMessages(locale.id); + document.documentElement.lang = locale.id; + document.title = messages.pageTitle; + document.querySelector('meta[name="description"]').content = messages.pageDescription; + localeSelector.value = locale.id; + document.querySelectorAll('[data-i18n]').forEach(element => { + element.textContent = messages[element.dataset.i18n]; + }); + document.querySelectorAll('[data-i18n-aria-label]').forEach(element => { + element.setAttribute('aria-label', messages[element.dataset.i18nAriaLabel]); + }); + document.querySelectorAll('.theme-toggle').forEach(button => { + button.dataset.lightLabel = messages.themeLight; + button.dataset.darkLabel = messages.themeDark; + button.dataset.switchToLight = messages.switchToLight; + button.dataset.switchToDark = messages.switchToDark; + }); + updateToggleIcon(); + } + function updateSelection(languageId) { const language = WorkshopLanguages.getLanguage(languageId); const hasLanguage = language !== null; - const workshop = selectedWorkshopId ? workshops[selectedWorkshopId] : null; + const workshop = selectedWorkshopId ? messages.workshops[selectedWorkshopId] : null; const ready = workshop !== null && hasLanguage; picker.disabled = workshop === null; @@ -77,43 +98,50 @@ System message: replace startLink.classList.toggle('disabled', !ready); startLink.setAttribute('aria-disabled', String(!ready)); startLink.href = ready - ? WorkshopLanguageNavigation.firstLessonUrl(language.id, selectedWorkshopId) + ? WorkshopLanguageNavigation.firstLessonUrl(language.id, selectedWorkshopId, locale.id) : workshop ? '#language-picker' : '#workshop-picker'; - startLink.textContent = ready ? `Start ${workshop.name}` : 'Start selected workshop'; + startLink.textContent = ready + ? WorkshopLocalization.format(messages.startWorkshop, { workshop: workshop.name }) + : messages.startSelected; targetAppLink.hidden = selectedWorkshopId !== 'sdlc'; if (!hasLanguage) { docsLink.removeAttribute('href'); docsLink.setAttribute('aria-disabled', 'true'); summary.textContent = workshop - ? `Now choose a language for ${workshop.name}.` - : 'Choose a workshop first, then select its implementation language.'; + ? WorkshopLocalization.format(messages.nowChooseLanguage, { workshop: workshop.name }) + : messages.chooseWorkshopThenLanguage; installCommand.textContent = ''; runtimeNote.textContent = ''; - startGuidance.textContent = workshop?.guidance ?? - 'Choose a workshop and language. No prior agent or SDK experience required.'; + startGuidance.textContent = workshop?.guidance ?? messages.startGuidance; return; } docsLink.href = language.docsUrl; docsLink.removeAttribute('aria-disabled'); - docsLink.textContent = `${language.displayName} SDK docs ↗`; + docsLink.textContent = WorkshopLocalization.format(messages.sdkDocsFor, { + language: language.displayName + }); summary.textContent = workshop - ? `${workshop.name} will use the ${language.displayName} SDK.` - : 'Choose a workshop to continue.'; + ? WorkshopLocalization.format(messages.workshopUsesSdk, { + workshop: workshop.name, + language: language.displayName + }) + : messages.chooseWorkshopToContinue; installCommand.textContent = language.installCommand; runtimeNote.textContent = language.runtimeNote; - startGuidance.textContent = workshop?.guidance ?? 'Choose a workshop to continue.'; + startGuidance.textContent = workshop?.guidance ?? messages.chooseWorkshopToContinue; } function selectWorkshop(workshopId) { - selectedWorkshopId = workshops[workshopId] ? workshopId : null; + selectedWorkshopId = workshopMetadata[workshopId] ? workshopId : null; document.querySelectorAll('.workshop-option').forEach(option => { option.classList.toggle('selected', option.dataset.workshop === selectedWorkshopId); }); - const workshop = selectedWorkshopId ? workshops[selectedWorkshopId] : null; - previewTitle.textContent = workshop?.previewTitle ?? 'workshop-preview'; - preview.textContent = workshop?.preview ?? 'Select a workshop to preview its agent flow.'; + const metadata = selectedWorkshopId ? workshopMetadata[selectedWorkshopId] : null; + const workshop = selectedWorkshopId ? messages.workshops[selectedWorkshopId] : null; + previewTitle.textContent = metadata?.previewTitle ?? 'workshop-preview'; + preview.textContent = workshop?.preview ?? messages.previewPlaceholder; updateSelection(languageInputs.find(input => input.checked)?.value ?? null); if (workshop) { languageInputs[0].focus(); @@ -132,6 +160,20 @@ System message: replace input.addEventListener('change', () => selectWorkshop(input.value)); }); + localeSelector.addEventListener('change', () => { + locale = WorkshopLocalization.getLocale(localeSelector.value); + storeValue(localeStorageKey, locale.id); + const url = new URL(window.location.href); + url.searchParams.set('locale', locale.id); + window.history.replaceState({}, '', url); + applyLocale(); + updateSelection(languageInputs.find(input => input.checked)?.value ?? null); + if (selectedWorkshopId) { + const workshop = messages.workshops[selectedWorkshopId]; + preview.textContent = workshop.preview; + } + }); + startLink.addEventListener('click', event => { if (startLink.getAttribute('aria-disabled') === 'true') { event.preventDefault(); @@ -144,6 +186,9 @@ System message: replace } }); + applyLocale(); + storeValue(localeStorageKey, locale.id); + const initialLanguage = WorkshopLanguageNavigation.resolveLanguage( window.location.search, getStoredLanguageId(), diff --git a/docs/index.html b/docs/index.html index 46fb7db..05d665e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -11,22 +11,30 @@ + - + @@ -34,39 +42,39 @@
-

Choose what your agent is here to do.

-

+

Choose what your agent is here to do.

+

Two workshops. One SDK. Build an agent for the software lifecycle, or take Copilot into a completely different domain.

- Choose a workshop + Choose a workshop
-

Choose your workshop language

-

Choose a workshop first, then select its implementation language.

+

Choose your workshop language

+

Choose a workshop first, then select its implementation language.

- Programming language + Programming language
- Start selected workshop -

Choose a workshop and language. No prior agent or SDK experience required.

+ Start selected workshop +

Choose a workshop and language. No prior agent or SDK experience required.

-
-

Build the app. Understand the boundary.

+

Build the app. Understand the boundary.

    -
  • Create and manage a Copilot session.
  • -
  • Separate durable agent policy from task data.
  • -
  • Choose the right tool surface for the job.
  • -
  • Validate objective output requirements in code.
  • -
  • Explain where prompt guidance ends and hard controls begin.
  • +
  • Create and manage a Copilot session.
  • +
  • Separate durable agent policy from task data.
  • +
  • Choose the right tool surface for the job.
  • +
  • Validate objective output requirements in code.
  • +
  • Explain where prompt guidance ends and hard controls begin.
diff --git a/docs/language-navigation.js b/docs/language-navigation.js index a123920..612e7d8 100644 --- a/docs/language-navigation.js +++ b/docs/language-navigation.js @@ -15,15 +15,18 @@ return getLanguage(storedLanguageId); } - function lessonUrl(stepId, languageId) { + function lessonUrl(stepId, languageId, locale) { const parameters = new URLSearchParams({ step: stepId }); if (languageId) { parameters.set('lang', languageId); } + if (locale) { + parameters.set('locale', locale); + } return `?${parameters.toString()}`; } - function homeUrl(languageId, workshopId) { + function homeUrl(languageId, workshopId, locale) { const parameters = new URLSearchParams(); if (languageId) { parameters.set('lang', languageId); @@ -31,15 +34,18 @@ if (workshopId) { parameters.set('workshop', workshopId); } + if (locale) { + parameters.set('locale', locale); + } const query = parameters.toString(); return query ? `../index.html?${query}` : '../index.html'; } - function firstLessonUrl(languageId, workshopId = 'sdlc') { + function firstLessonUrl(languageId, workshopId = 'sdlc', locale) { const firstStep = workshopId === 'museum' ? 'museum-00-preflight' : '00-preflight'; - return `workshop/step.html${lessonUrl(firstStep, languageId)}`; + return `workshop/step.html${lessonUrl(firstStep, languageId, locale)}`; } function siteRootUrl(lessonPageUrl) { diff --git a/docs/localization.js b/docs/localization.js new file mode 100644 index 0000000..e60338c --- /dev/null +++ b/docs/localization.js @@ -0,0 +1,291 @@ +(function (root, factory) { + const api = factory(); + if (typeof module === 'object' && module.exports) { + module.exports = api; + } + root.WorkshopLocalization = api; +}(typeof globalThis !== 'undefined' ? globalThis : this, function () { + 'use strict'; + + const defaultLocale = 'en'; + const locales = Object.freeze([ + Object.freeze({ id: 'en', displayName: 'English' }), + Object.freeze({ id: 'pt-BR', displayName: 'Português (Brasil)' }) + ]); + + const messages = Object.freeze({ + en: Object.freeze({ + pageTitle: 'Choose your workshop | GitHub Copilot SDK', + pageDescription: 'Choose a hands-on GitHub Copilot SDK workshop: build an SDLC accessibility reviewer or a grounded museum curator.', + skipLink: 'Skip to workshop overview', + homeLabel: 'GitHub Copilot SDK Workshop home', + resourcesLabel: 'Workshop resources', + targetApp: 'Target app ↗', + sdkDocs: '📚 SDK docs ↗', + localeLabel: 'Content language', + themeLight: 'Light', + themeDark: 'Dark', + switchToLight: 'Switch to light theme', + switchToDark: 'Switch to dark theme', + heroTitle: 'Choose what your agent is here to do.', + heroDefinition: 'Two workshops. One SDK. Build an agent for the software lifecycle, or take Copilot into a completely different domain.', + chooseWorkshop: 'Choose a workshop', + accessibilityKicker: 'Developer tool · 90 minutes', + accessibilityTitle: 'Review web accessibility', + accessibilityDescription: 'Build an SDLC agent that inspects a page, consults WCAG guidance, and produces an evidence-based report.', + accessibilityCapabilities: 'Streaming · local tools · Playwright MCP · permissions', + museumKicker: 'Non-SDLC tool · 90 minutes', + museumTitle: 'Curate a museum exhibit', + museumDescription: 'Build a grounded interpretive agent that turns approved facts into visitor-ready exhibit copy.', + museumCapabilities: 'Custom persona · one application-owned tool · validation · evaluation', + chooseLanguage: 'Choose your workshop language', + chooseWorkshopFirst: 'Choose a workshop first, then select its implementation language.', + programmingLanguage: 'Programming language', + startSelected: 'Start selected workshop', + startGuidance: 'Choose a workshop and language. No prior agent or SDK experience required.', + previewLabel: 'Selected workshop preview', + previewPlaceholder: 'Select a workshop to preview its agent flow.', + outcomesTitle: 'Build the app. Understand the boundary.', + outcomeSession: 'Create and manage a Copilot session.', + outcomePolicy: 'Separate durable agent policy from task data.', + outcomeTools: 'Choose the right tool surface for the job.', + outcomeValidation: 'Validate objective output requirements in code.', + outcomeControls: 'Explain where prompt guidance ends and hard controls begin.', + lesson: Object.freeze({ + pageDescription: 'Self-guided, multilingual workshops for building agents with the GitHub Copilot SDK.', + skipLink: 'Skip to lesson', + openSections: 'Open sections', + closeSections: 'Close sections', + homeLabel: 'Copilot SDK Workshop home', + actionsLabel: 'Lesson actions', + languageLabel: 'Language', + languageAriaLabel: 'Workshop programming language', + chooseLanguage: 'Choose language', + hub: '🏠 Hub', + docs: '📚 Docs ↗', + previous: 'Previous', + next: 'Next', + loading: 'Loading', + loadingLesson: 'Loading lesson.', + loadingLessonEllipsis: 'Loading lesson...', + progressLabel: 'Core workshop progress', + workshopSteps: 'Workshop steps', + paginationLabel: 'Lesson pagination', + beforeBegin: 'Before you begin', + coreWorkshop: 'Core workshop', + optionalExtension: 'Optional extension', + preflight: 'Preflight', + stepOf: 'Step {number} of {count}', + docsFor: '📚 {language} docs ↗', + chooseLanguagePageTitle: 'Choose a language | Copilot SDK Workshop', + chooseLanguageHeading: 'Choose a workshop language', + chooseLanguageDetails: 'Select one of the six supported languages above before loading a lesson.', + chooseLanguageStatus: 'Choose a workshop language to continue.', + copy: 'Copy', + copied: 'Copied', + copyFailed: 'Copy failed', + copyCodeBlock: 'Copy code block', + unableToLoad: 'Unable to load this lesson', + serveLocally: 'Serve the repository with a local HTTP server; browsers block lesson fetches from file URLs.', + refreshGuidance: 'Refresh the page. If the problem continues, verify that the workshop Markdown was deployed.', + unableStatus: 'Unable to load this lesson.', + lessonRequestFailed: 'Lesson request failed with HTTP {status}.', + loadedStatus: 'Loaded {title}.', + loadingStatus: 'Loading {title}.', + fallbackNotice: 'This lesson is not available in Portuguese yet. Showing the English version.' + }), + steps: Object.freeze({}), + nowChooseLanguage: 'Now choose a language for {workshop}.', + chooseWorkshopThenLanguage: 'Choose a workshop first, then select its implementation language.', + workshopUsesSdk: '{workshop} will use the {language} SDK.', + chooseWorkshopToContinue: 'Choose a workshop to continue.', + sdkDocsFor: '{language} SDK docs ↗', + startWorkshop: 'Start {workshop}', + workshops: Object.freeze({ + sdlc: Object.freeze({ + name: 'Accessibility Reviewer', + guidance: 'Build an SDLC developer tool in a 90-minute core workshop.', + preview: `URL → Playwright inspection + → WCAG lookup + → structured report + +[tool] playwright-browser_navigate +[tool] accessibility_rule_lookup + +Finding +The name input has no accessible name.` + }), + museum: Object.freeze({ + name: 'Museum Exhibit Studio', + guidance: 'Build a non-SDLC curator tool in a 90-minute core workshop.', + preview: `Approved facts → curator session + → exhibit validation + → visitor-ready copy + +Available tools: [] +System message: replace + +# Journey to the Moon +## Narrative +## Visitor questions` + }) + }) + }), + 'pt-BR': Object.freeze({ + pageTitle: 'Escolha seu workshop | GitHub Copilot SDK', + pageDescription: 'Escolha um workshop prático do GitHub Copilot SDK: crie um revisor de acessibilidade para SDLC ou um curador de museu fundamentado em fatos.', + skipLink: 'Pular para a visão geral do workshop', + homeLabel: 'Página inicial do GitHub Copilot SDK Workshop', + resourcesLabel: 'Recursos do workshop', + targetApp: 'Aplicativo-alvo ↗', + sdkDocs: '📚 Documentação do SDK ↗', + localeLabel: 'Idioma do conteúdo', + themeLight: 'Claro', + themeDark: 'Escuro', + switchToLight: 'Mudar para o tema claro', + switchToDark: 'Mudar para o tema escuro', + heroTitle: 'Escolha o que seu agente fará.', + heroDefinition: 'Dois workshops. Um SDK. Crie um agente para o ciclo de vida de software ou leve o Copilot para um domínio completamente diferente.', + chooseWorkshop: 'Escolha um workshop', + accessibilityKicker: 'Ferramenta para desenvolvedores · 90 minutos', + accessibilityTitle: 'Revise a acessibilidade da Web', + accessibilityDescription: 'Crie um agente de SDLC que inspeciona uma página, consulta orientações das WCAG e produz um relatório baseado em evidências.', + accessibilityCapabilities: 'Streaming · ferramentas locais · Playwright MCP · permissões', + museumKicker: 'Ferramenta fora de SDLC · 90 minutos', + museumTitle: 'Crie a curadoria de uma exposição', + museumDescription: 'Crie um agente interpretativo fundamentado que transforma fatos aprovados em textos de exposição prontos para visitantes.', + museumCapabilities: 'Persona personalizada · uma ferramenta do aplicativo · validação · avaliação', + chooseLanguage: 'Escolha a linguagem de programação', + chooseWorkshopFirst: 'Primeiro escolha um workshop. Depois, selecione a linguagem da implementação.', + programmingLanguage: 'Linguagem de programação', + startSelected: 'Iniciar workshop selecionado', + startGuidance: 'Escolha um workshop e uma linguagem. Não é necessário ter experiência prévia com agentes ou com o SDK.', + previewLabel: 'Prévia do workshop selecionado', + previewPlaceholder: 'Selecione um workshop para visualizar o fluxo do agente.', + outcomesTitle: 'Crie o aplicativo. Entenda os limites.', + outcomeSession: 'Crie e gerencie uma sessão do Copilot.', + outcomePolicy: 'Separe a política permanente do agente dos dados da tarefa.', + outcomeTools: 'Escolha a superfície de ferramentas adequada para o trabalho.', + outcomeValidation: 'Valide requisitos objetivos de saída no código.', + outcomeControls: 'Explique onde termina a orientação do prompt e começam os controles rígidos.', + lesson: Object.freeze({ + pageDescription: 'Workshops autoguiados e multilíngues para criar agentes com o GitHub Copilot SDK.', + skipLink: 'Pular para a lição', + openSections: 'Abrir seções', + closeSections: 'Fechar seções', + homeLabel: 'Página inicial do Copilot SDK Workshop', + actionsLabel: 'Ações da lição', + languageLabel: 'Linguagem', + languageAriaLabel: 'Linguagem de programação do workshop', + chooseLanguage: 'Escolha uma linguagem', + hub: '🏠 Início', + docs: '📚 Documentação ↗', + previous: 'Anterior', + next: 'Próxima', + loading: 'Carregando', + loadingLesson: 'Carregando lição.', + loadingLessonEllipsis: 'Carregando lição...', + progressLabel: 'Progresso do workshop principal', + workshopSteps: 'Etapas do workshop', + paginationLabel: 'Paginação das lições', + beforeBegin: 'Antes de começar', + coreWorkshop: 'Workshop principal', + optionalExtension: 'Extensão opcional', + preflight: 'Preparação', + stepOf: 'Etapa {number} de {count}', + docsFor: '📚 Documentação de {language} ↗', + chooseLanguagePageTitle: 'Escolha uma linguagem | Copilot SDK Workshop', + chooseLanguageHeading: 'Escolha a linguagem de programação', + chooseLanguageDetails: 'Selecione uma das seis linguagens disponíveis acima antes de carregar uma lição.', + chooseLanguageStatus: 'Escolha uma linguagem de programação para continuar.', + copy: 'Copiar', + copied: 'Copiado', + copyFailed: 'Falha ao copiar', + copyCodeBlock: 'Copiar bloco de código', + unableToLoad: 'Não foi possível carregar esta lição', + serveLocally: 'Sirva o repositório com um servidor HTTP local. Os navegadores bloqueiam solicitações de lições em URLs file://.', + refreshGuidance: 'Atualize a página. Se o problema continuar, verifique se o Markdown do workshop foi publicado.', + unableStatus: 'Não foi possível carregar esta lição.', + lessonRequestFailed: 'A solicitação da lição falhou com o status HTTP {status}.', + loadedStatus: '{title} foi carregada.', + loadingStatus: 'Carregando {title}.', + fallbackNotice: 'Esta lição ainda não está disponível em português. A versão em inglês será exibida.' + }), + steps: Object.freeze({ + '00-preflight': Object.freeze({ title: 'Prepare sua máquina', navTitle: 'Preparação', time: 'Sem tempo definido' }), + '01-first-session': Object.freeze({ title: 'Crie sua primeira sessão do Copilot', navTitle: 'Primeira sessão' }), + '02-streaming': Object.freeze({ title: 'Transmita uma resposta', navTitle: 'Streaming' }), + '03-local-tool': Object.freeze({ title: 'Adicione conhecimento controlado pelo aplicativo', navTitle: 'Ferramenta local' }), + '04-mcp-safety': Object.freeze({ title: 'Conecte uma ferramenta externa com segurança', navTitle: 'MCP e permissões' }), + '05-combine-tools': Object.freeze({ title: 'Combine ferramentas locais e MCP', navTitle: 'Combine ferramentas' }), + '06-structured-report': Object.freeze({ title: 'Produza um relatório estruturado', navTitle: 'Relatório estruturado' }), + '07-run-explain': Object.freeze({ title: 'Execute e explique o aplicativo', navTitle: 'Execute e explique' }), + '08-model-selection': Object.freeze({ title: 'Selecione um modelo', navTitle: 'Seleção de modelo' }), + '09-interactive-html-report': Object.freeze({ title: 'Gere um relatório HTML interativo', navTitle: 'Relatório interativo' }), + 'museum-00-preflight': Object.freeze({ title: 'Prepare o Museum Exhibit Studio', navTitle: 'Preparação', time: 'Sem tempo definido' }), + 'museum-01-first-curator-session': Object.freeze({ title: 'Crie sua primeira sessão de curadoria', navTitle: 'Primeira sessão' }), + 'museum-02-stream-the-curator': Object.freeze({ title: 'Transmita a resposta do curador', navTitle: 'Streaming' }), + 'museum-03-curator-voice': Object.freeze({ title: 'Dê uma voz ao curador', navTitle: 'Voz do curador' }), + 'museum-04-approved-facts': Object.freeze({ title: 'Fundamente a resposta em fatos aprovados', navTitle: 'Fatos aprovados' }), + 'museum-05-guardrails': Object.freeze({ title: 'Defina as proteções', navTitle: 'Proteções' }), + 'museum-06-prove-the-structure': Object.freeze({ title: 'Comprove a estrutura', navTitle: 'Verificações estruturais' }), + 'museum-07-wikipedia-research': Object.freeze({ title: 'Pesquise com o MCP da Wikipedia', navTitle: 'Pesquisa na Wikipedia' }), + 'museum-08-interactive-exhibit-page': Object.freeze({ title: 'Publique uma página interativa da exposição', navTitle: 'Página da exposição' }) + }), + nowChooseLanguage: 'Agora escolha uma linguagem para {workshop}.', + chooseWorkshopThenLanguage: 'Primeiro escolha um workshop. Depois, selecione a linguagem da implementação.', + workshopUsesSdk: '{workshop} usará o SDK para {language}.', + chooseWorkshopToContinue: 'Escolha um workshop para continuar.', + sdkDocsFor: 'Documentação do SDK para {language} ↗', + startWorkshop: 'Iniciar {workshop}', + workshops: Object.freeze({ + sdlc: Object.freeze({ + name: 'Accessibility Reviewer', + guidance: 'Crie uma ferramenta de SDLC para desenvolvedores no workshop principal de 90 minutos.', + preview: `URL → inspeção com Playwright + → consulta às WCAG + → relatório estruturado + +[ferramenta] playwright-browser_navigate +[ferramenta] accessibility_rule_lookup + +Problema encontrado +O campo de nome não tem um nome acessível.` + }), + museum: Object.freeze({ + name: 'Museum Exhibit Studio', + guidance: 'Crie uma ferramenta de curadoria fora de SDLC no workshop principal de 90 minutos.', + preview: `Fatos aprovados → sessão do curador + → validação da exposição + → texto pronto para visitantes + +Ferramentas disponíveis: [] +Mensagem do sistema: substituir + +# Viagem à Lua +## Narrativa +## Perguntas dos visitantes` + }) + }) + }) + }); + + function getLocale(localeId) { + return locales.find(locale => locale.id.toLowerCase() === localeId?.toLowerCase()) ?? null; + } + + function resolveLocale(search, storedLocaleId) { + const requestedLocale = new URLSearchParams(search).get('locale'); + return getLocale(requestedLocale) ?? getLocale(storedLocaleId) ?? getLocale(defaultLocale); + } + + function getMessages(localeId) { + return messages[getLocale(localeId)?.id ?? defaultLocale]; + } + + function format(message, values = {}) { + return message.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match); + } + + return Object.freeze({ defaultLocale, format, getLocale, getMessages, locales, resolveLocale }); +})); \ No newline at end of file diff --git a/docs/step.css b/docs/step.css index f1e83e8..f7bc207 100644 --- a/docs/step.css +++ b/docs/step.css @@ -858,6 +858,20 @@ button { font-size: 0.8rem; } +.locale-select { + max-width: 10.5rem; +} + +.translation-notice { + margin-bottom: 1rem; + padding: 0.75rem 1rem; + border: 1px solid var(--warning-yellow); + border-radius: 6px; + background: color-mix(in srgb, var(--warning-yellow) 10%, var(--bg-card)); + color: var(--text-primary); + font-size: 0.875rem; +} + .nav-btn, .header .theme-toggle { display: inline-flex; @@ -1191,6 +1205,10 @@ button { max-width: 6.5rem; } + .locale-select { + max-width: 7.5rem; + } + .header-nav { margin-left: auto; gap: 0.3rem; diff --git a/docs/styles.css b/docs/styles.css index cacdc35..0c476bf 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -147,6 +147,18 @@ a { border-radius: 6px; } +.locale-control select { + max-width: 11rem; + padding: 0.45rem 1.75rem 0.45rem 0.65rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); + font: inherit; + font-size: 0.86rem; + font-weight: 600; +} + .home-shell { width: min(1180px, calc(100% - 2.5rem)); min-height: calc(100svh - 4.5rem); @@ -531,6 +543,10 @@ h1 { gap: 0.5rem; } + .locale-control select { + max-width: 8.5rem; + } + .site-nav a { display: none; } diff --git a/docs/tests/markdown-language-preprocessor.test.js b/docs/tests/markdown-language-preprocessor.test.js index b892356..b94c61d 100644 --- a/docs/tests/markdown-language-preprocessor.test.js +++ b/docs/tests/markdown-language-preprocessor.test.js @@ -10,6 +10,12 @@ const { siteRootUrl } = require('../language-navigation.js'); const { preprocessLanguageDirectives } = require('../markdown-language-preprocessor.js'); +const { + format, + getLocale, + getMessages, + resolveLocale +} = require('../localization.js'); const preprocess = (markdown, languageId) => preprocessLanguageDirectives(markdown, languageId, getLanguage); @@ -33,10 +39,22 @@ assert.throws(() => preprocess(':::unexpected', 'dotnet'), /expected :::language assert.equal(lessonUrl('04-mcp-safety', 'rust'), '?step=04-mcp-safety&lang=rust'); assert.equal(lessonUrl('04-mcp-safety'), '?step=04-mcp-safety'); +assert.equal( + lessonUrl('04-mcp-safety', 'rust', 'pt-BR'), + '?step=04-mcp-safety&lang=rust&locale=pt-BR' +); assert.equal(firstLessonUrl('java'), 'workshop/step.html?step=00-preflight&lang=java'); assert.equal(firstLessonUrl('python', 'museum'), 'workshop/step.html?step=museum-00-preflight&lang=python'); +assert.equal( + firstLessonUrl('python', 'museum', 'pt-BR'), + 'workshop/step.html?step=museum-00-preflight&lang=python&locale=pt-BR' +); assert.equal(homeUrl('python'), '../index.html?lang=python'); assert.equal(homeUrl('python', 'museum'), '../index.html?lang=python&workshop=museum'); +assert.equal( + homeUrl('python', 'museum', 'pt-BR'), + '../index.html?lang=python&workshop=museum&locale=pt-BR' +); assert.equal(homeUrl(), '../index.html'); assert.equal( siteRootUrl('https://expert-adventure-l67eo16.pages.github.io/workshop/step.html?step=00-preflight').href, @@ -50,4 +68,19 @@ assert.equal(resolveLanguage('?lang=go', 'rust', getLanguage).id, 'go'); assert.equal(resolveLanguage('', 'rust', getLanguage).id, 'rust'); assert.equal(resolveLanguage('?lang=unknown', 'rust', getLanguage), null); +assert.equal(resolveLocale('', null).id, 'en'); +assert.equal(resolveLocale('?locale=pt-BR', null).id, 'pt-BR'); +assert.equal(resolveLocale('?locale=PT-br', null).id, 'pt-BR'); +assert.equal(resolveLocale('', 'pt-BR').id, 'pt-BR'); +assert.equal(resolveLocale('?locale=unknown', 'pt-BR').id, 'pt-BR'); +assert.equal(getLocale('unknown'), null); +assert.equal(getMessages('unknown').pageTitle, getMessages('en').pageTitle); +assert.equal( + format('{workshop} usará o SDK para {language}.', { + workshop: 'Accessibility Reviewer', + language: 'Python' + }), + 'Accessibility Reviewer usará o SDK para Python.' +); + console.log('Workshop language directive and navigation tests passed.'); diff --git a/docs/theme-toggle.js b/docs/theme-toggle.js index 7589c49..3e00853 100644 --- a/docs/theme-toggle.js +++ b/docs/theme-toggle.js @@ -14,8 +14,12 @@ function toggleTheme() { function updateToggleIcon() { const isLight = document.documentElement.dataset.theme === 'light'; document.querySelectorAll('.theme-toggle').forEach(button => { - button.textContent = isLight ? '🌙 Dark' : '☀️ Light'; - button.setAttribute('aria-label', `Switch to ${isLight ? 'dark' : 'light'} theme`); + const lightLabel = button.dataset.lightLabel ?? 'Light'; + const darkLabel = button.dataset.darkLabel ?? 'Dark'; + const switchToLight = button.dataset.switchToLight ?? 'Switch to light theme'; + const switchToDark = button.dataset.switchToDark ?? 'Switch to dark theme'; + button.textContent = isLight ? `🌙 ${darkLabel}` : `☀️ ${lightLabel}`; + button.setAttribute('aria-label', isLight ? switchToDark : switchToLight); }); } diff --git a/docs/workshop/step.html b/docs/workshop/step.html index 76d598f..566e454 100644 --- a/docs/workshop/step.html +++ b/docs/workshop/step.html @@ -14,12 +14,13 @@ + - +
@@ -37,10 +38,10 @@ Loading lesson -
-