feat: add full-guide language toggle
Document the client-side, dual-rendered locale contract and dispatch a narrow language-change event for guide selector panels.\n\nDo not assemble the full-guide page or change its content collections; task 15d owns that integration.
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# Context: full-guide language switching
|
||||
|
||||
## Decision
|
||||
|
||||
The Astro full guide keeps the current client-side language switch on its
|
||||
existing `/full-guide/` URL. It server-renders both locale variants and the
|
||||
language-toggle island shows the selected variant after `client:idle` hydration.
|
||||
|
||||
This deliberately preserves the current no-URL-change contract, including links
|
||||
shared without a locale segment, and avoids a route/redirect and publishing
|
||||
change. The cost is duplicated localized HTML and both locales in the response.
|
||||
That is acceptable for this small guide and avoids sending duplicated string
|
||||
data through every interactive island.
|
||||
|
||||
## Markup and island contract for task 15d
|
||||
|
||||
- Render each static localized fragment twice. Put `data-language-content="en"`
|
||||
or `data-language-content="pt"` on its outer element. English is visible in
|
||||
server HTML; the toggle uses the native `hidden` attribute for the inactive
|
||||
locale.
|
||||
- Add `<LanguageToggle />` to the guide top bar. Astro's `client:idle` directive
|
||||
is only valid for framework components; this `.astro` island defers its
|
||||
browser setup with `requestIdleCallback` (and a timeout fallback) instead. Do
|
||||
not hydrate the page or use `client:load`; the control is deliberately
|
||||
idle-priority.
|
||||
- The island owns the `ai-for-dummies-language` localStorage key. Every read and
|
||||
write remains inside `try`/`catch`, because previews may disable storage.
|
||||
- On each selection the island sets `<html lang>` to `en` or `pt-BR`, updates
|
||||
its `[data-lang]` buttons' `.active` class and `aria-pressed` state, updates
|
||||
`[data-language-content]`, then dispatches `ai-for-dummies:languagechange` on
|
||||
`window`. The event detail is `{ language: 'en' | 'pt' }`.
|
||||
- The guide selector island (15a) must read `document.documentElement.lang` when
|
||||
it hydrates and listen for that event. On receipt it must re-render the
|
||||
currently active phase and all active selector panels from their collection
|
||||
data. This preserves today’s `applyLanguage` behaviour without coupling the
|
||||
toggle to page selectors.
|
||||
|
||||
This is a page-local contract: the existing `/rules/` toggle continues using its
|
||||
own `rules-language` key and must not be changed as part of full-guide
|
||||
migration.
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
// LanguageToggle — idle-hydrated, full-guide-only locale control.
|
||||
//
|
||||
// The guide server-renders both locales. This leaf owns the persisted locale
|
||||
// and announces changes so selector islands can rebuild their active panels
|
||||
// from collection data without knowing about this control's markup.
|
||||
---
|
||||
|
||||
<div class="lang-switch" aria-label="Language" data-language-toggle>
|
||||
<button class="active" data-lang="en" aria-pressed="true">EN</button>
|
||||
<span>/</span>
|
||||
<button data-lang="pt" aria-pressed="false">PT</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
type Language = 'en' | 'pt';
|
||||
|
||||
function setup(root: HTMLElement) {
|
||||
const buttons = root.querySelectorAll<HTMLButtonElement>('[data-lang]');
|
||||
|
||||
function renderLanguage(next: Language) {
|
||||
document.documentElement.lang = next === 'pt' ? 'pt-BR' : 'en';
|
||||
buttons.forEach((button) => {
|
||||
const isActive = button.dataset.lang === next;
|
||||
button.classList.toggle('active', isActive);
|
||||
button.setAttribute('aria-pressed', String(isActive));
|
||||
});
|
||||
document.querySelectorAll<HTMLElement>('[data-language-content]').forEach((node) => {
|
||||
node.hidden = node.dataset.languageContent !== next;
|
||||
});
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('ai-for-dummies:languagechange', { detail: { language: next } }),
|
||||
);
|
||||
}
|
||||
|
||||
function selectLanguage(value: string | undefined): Language {
|
||||
return value === 'pt' ? 'pt' : 'en';
|
||||
}
|
||||
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const language = selectLanguage(button.dataset.lang);
|
||||
renderLanguage(language);
|
||||
try {
|
||||
localStorage.setItem('ai-for-dummies-language', language);
|
||||
} catch {
|
||||
// Preview environments can disable storage.
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let savedLanguage = 'en';
|
||||
try {
|
||||
savedLanguage = localStorage.getItem('ai-for-dummies-language') || 'en';
|
||||
} catch {
|
||||
// Preview environments can disable storage.
|
||||
}
|
||||
renderLanguage(selectLanguage(savedLanguage));
|
||||
}
|
||||
|
||||
document.querySelectorAll<HTMLElement>('[data-language-toggle]').forEach((root) => {
|
||||
if ('requestIdleCallback' in window) {
|
||||
window.requestIdleCallback(() => setup(root));
|
||||
} else {
|
||||
setTimeout(() => setup(root), 0);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user