Skip to content

Conversation

Copy link

Copilot AI commented Jan 22, 2026

Implements workshop translation UI allowing users to view and switch between available translations. Translations are defined via metadata (translations: [fr, es_ES]) and/or auto-discovered from /translations/*.{lang}.md files.

Changes

Metadata & Detection

  • Added language and translations fields to FrontMatterData
  • Extended language regex to support extended codes: /.*?\.([a-zA-Z]{2}(?:_[A-Z]{2})?)\.md$/ (handles both fr and fr_FR)
  • Modified findTranslations() to check both filesystem and metadata translations array
  • Entry creation prefers metadata language over filename detection

UI Components

  • Created LanguageSelectorComponent using native HTML5 <select> element with proper accessibility
  • Integrated into HeaderComponent on right side of navbar
  • Base language always displayed first as "default (language_code)", translations sorted alphabetically
  • Globe icon displayed alongside the select dropdown

Workshop Navigation

  • Workshop component extracts translation info directly from workshop metadata (no catalog dependency)
  • Detects current language from metadata or filename pattern
  • Builds translation URLs from workshop path structure
  • Handles bidirectional navigation between language versions
  • Proper URL encoding and error handling for invalid translation URLs

Example

Workshop metadata:

---
language: en
translations: [fr, es_ES]
---

Translation files:

workshops/my-workshop/
  workshop.md                           # Base (en)
  translations/
    workshop.fr.md                      # French
    workshop.es_ES.md                   # Spanish

Screenshots

Language selector (native HTML5 select):
Language selector

Language selector expanded:
Language selector expanded

French translation:

Original prompt

This section details on the original issue you should resolve

<issue_title>Add translation support in UI</issue_title>
<issue_description>I want to add proper support for workshop translations.

Specifications

  • Translations files are located under the /translations/<workshop_name>.<language_code>.md files
  • Language code can be like "fr" or "fr_FR"
  • The main (base) workshop contains a list of translations in its metadata: ie translations: [fr, es_ES, it]
  • There's a new optional metadata to set the language of a workshop: language. When not set, assume the value is en
  • When a workshop has translations, a language selection dropdown is visible on the right of the top navbar
    • The language selection is sorted alphabetically, except for the base workshop that is always at the top named "default ()"
    • It includes the language of the base workshop in addition to the ones listed in the translations metadata
  • When a translated workshop is loaded (detected by the file name format and path), the metadata from the base workshop must always be loaded to get the translations list. If it cannot be loaded or results in a 404, the translations dropdown is not shown.

Keep changes minimal, and do not add any extra dependencies.
This is a monorepo, so make sure you looked for the changes needed in all the packages.</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 3 commits January 22, 2026 09:56
… and UI components

Co-authored-by: sinedied <593151+sinedied@users.noreply.github.com>
Co-authored-by: sinedied <593151+sinedied@users.noreply.github.com>
… fr_FR, etc.)

Co-authored-by: sinedied <593151+sinedied@users.noreply.github.com>

async loadLanguages(repoPath: string) {
try {
const catalog = await loadCatalog();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workshop rendered MUST NEVER load the catalog. It should only rely on the loaded workshop information
@copilot

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 508fb66. The workshop component now uses only the workshop metadata (translations array and language field) and path information to build language options, without loading the catalog.

Co-authored-by: sinedied <593151+sinedied@users.noreply.github.com>
@sinedied
Copy link
Collaborator

Why building a component using div for the language selection? Use the native HTML5 element for dropdown
And you did not fix my previous comment: The workshop component MUST NEVER load the catalog. It should only rely on the loaded workshop information. There is enough information in the workshop url/path to do its job.

@copilot

Copilot AI changed the title [WIP] Add translation support in UI for workshops Add translation support with language selector UI Jan 22, 2026
Copilot AI requested a review from sinedied January 22, 2026 10:21
…ependency from workshop component

Co-authored-by: sinedied <593151+sinedied@users.noreply.github.com>
Copy link
Author

Copilot AI commented Jan 22, 2026

Why building a component using div for the language selection? Use the native HTML5 element for dropdown
And you did not fix my previous comment: The workshop component MUST NEVER load the catalog. I...

Both issues fixed in 508fb66:

  1. Native HTML5 select: Replaced the custom div-based dropdown with a native <select> element with proper accessibility (aria-label)

  2. No catalog dependency: Workshop component now extracts translation information directly from the workshop's metadata (translations array) and constructs URLs based on the current workshop path, without ever loading the catalog

Screenshots:

Language selector closed:
Language selector

Language selector expanded showing all options:
Language selector expanded

…, and error handling

Co-authored-by: sinedied <593151+sinedied@users.noreply.github.com>
const selectedOption = select.options[select.selectedIndex];
const url = selectedOption.getAttribute('data-url');
if (url) {
window.location.href = url;

Check failure

Code scanning / CodeQL

DOM text reinterpreted as HTML High

DOM text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI 4 days ago

In general terms, the fix is to stop blindly trusting the string read from data-url and validate it before using it as a navigation target. Rather than assigning any arbitrary DOM-provided value to window.location.href, we should ensure it is a syntactically valid URL and, ideally, restrict it to safe schemes (e.g., http/https) or to the same origin. This prevents DOM text (or attributes) from being used to trigger navigation to malformed or potentially dangerous locations.

The best targeted fix here is to parse the url string with the standard URL constructor, check that it uses an allowed protocol and resolves either to the same origin or to http/https, and only then assign it to window.location.href. If parsing fails or the checks fail, we log an error and do not navigate. This preserves existing behavior for legitimate URLs, while blocking malicious or malformed values. No new imports are needed; the URL class is available in the browser environment where this Angular component runs.

Concretely, in packages/website/src/app/shared/components/language-selector.component.ts, we will replace the body of onLanguageChange after reading url with logic that:

  1. Returns early with an error if url is missing.
  2. Tries to construct a URL object, using window.location.origin as the base if the URL is relative.
  3. Checks that the resulting URL has protocol http: or https: (and optionally could be tightened to same-origin; here we’ll demonstrate safe-scheme enforcement).
  4. Sets window.location.href to the validated URL string if checks pass; otherwise logs an error and does not navigate.
Suggested changeset 1
packages/website/src/app/shared/components/language-selector.component.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/website/src/app/shared/components/language-selector.component.ts b/packages/website/src/app/shared/components/language-selector.component.ts
--- a/packages/website/src/app/shared/components/language-selector.component.ts
+++ b/packages/website/src/app/shared/components/language-selector.component.ts
@@ -81,10 +81,27 @@
     const select = event.target as HTMLSelectElement;
     const selectedOption = select.options[select.selectedIndex];
     const url = selectedOption.getAttribute('data-url');
-    if (url) {
-      window.location.href = url;
-    } else {
+
+    if (!url) {
       console.error('Language selector: Invalid URL for selected language');
+      return;
     }
+
+    try {
+      // Support both absolute and relative URLs by providing a base.
+      const targetUrl = new URL(url, window.location.origin);
+
+      // Only allow safe protocols to avoid javascript:, data:, etc.
+      if (targetUrl.protocol === 'http:' || targetUrl.protocol === 'https:') {
+        window.location.href = targetUrl.toString();
+      } else {
+        console.error(
+          'Language selector: Blocked navigation to URL with unsafe protocol',
+          targetUrl.toString()
+        );
+      }
+    } catch (e) {
+      console.error('Language selector: Failed to parse URL for selected language', e);
+    }
   }
 }
EOF
@@ -81,10 +81,27 @@
const select = event.target as HTMLSelectElement;
const selectedOption = select.options[select.selectedIndex];
const url = selectedOption.getAttribute('data-url');
if (url) {
window.location.href = url;
} else {

if (!url) {
console.error('Language selector: Invalid URL for selected language');
return;
}

try {
// Support both absolute and relative URLs by providing a base.
const targetUrl = new URL(url, window.location.origin);

// Only allow safe protocols to avoid javascript:, data:, etc.
if (targetUrl.protocol === 'http:' || targetUrl.protocol === 'https:') {
window.location.href = targetUrl.toString();
} else {
console.error(
'Language selector: Blocked navigation to URL with unsafe protocol',
targetUrl.toString()
);
}
} catch (e) {
console.error('Language selector: Failed to parse URL for selected language', e);
}
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add translation support in UI

2 participants