From 6a4b6d1093cac68bb6c930b212e1edffca0e163c Mon Sep 17 00:00:00 2001
From: Marcelo Santos <117441129+marcelo-m7@users.noreply.github.com>
Date: Fri, 19 Sep 2025 16:50:52 +0100
Subject: [PATCH] Remove legacy root content duplicates
---
.gitignore | 9 +-
README.md | 40 +++--
config/_default/hugo.toml | 2 +-
config/_default/languages.toml | 22 ++-
config/_default/menus/menus.es.toml | 9 ++
config/_default/menus/menus.fr.toml | 9 ++
config/_default/params.toml | 4 -
content/en/_index.md | 2 +-
content/es/_index.md | 7 +
content/fr/_index.md | 7 +
content/{ => pt}/_index.md | 0
.../{ => pt}/courses/LESTI/2024-2025/index.md | 0
.../uc/LESTI-ALG1/estruturas-de-dados.md | 0
.../LESTI/2024-2025/uc/LESTI-ALG1/index.md | 0
hugo_stats.json | 16 +-
i18n/en.yaml | 145 ++++++++++++++++++
i18n/es.yaml | 145 ++++++++++++++++++
i18n/fr.yaml | 145 ++++++++++++++++++
i18n/pt.yaml | 145 ++++++++++++++++++
layouts/_default/baseof.html | 9 +-
.../footer/script-footer-custom.html | 6 -
layouts/_partials/i18n/messages.json | 24 +++
layouts/course/single.html | 24 +--
layouts/home.html | 13 +-
layouts/topic/single.html | 14 +-
layouts/uc/single.html | 30 ++--
netlify.toml | 2 +-
package-lock.json | 9 +-
package.json | 3 +-
scripts/sync-content.mjs | 53 +++++--
scripts/validate-frontmatter.js | 47 ++++--
static/js/loaders.js | 139 ++++++++++++-----
static/js/supabaseClient.js | 17 +-
33 files changed, 952 insertions(+), 145 deletions(-)
create mode 100644 config/_default/menus/menus.es.toml
create mode 100644 config/_default/menus/menus.fr.toml
create mode 100644 content/es/_index.md
create mode 100644 content/fr/_index.md
rename content/{ => pt}/_index.md (100%)
rename content/{ => pt}/courses/LESTI/2024-2025/index.md (100%)
rename content/{ => pt}/courses/LESTI/2024-2025/uc/LESTI-ALG1/estruturas-de-dados.md (100%)
rename content/{ => pt}/courses/LESTI/2024-2025/uc/LESTI-ALG1/index.md (100%)
create mode 100644 i18n/en.yaml
create mode 100644 i18n/es.yaml
create mode 100644 i18n/fr.yaml
create mode 100644 i18n/pt.yaml
create mode 100644 layouts/_partials/i18n/messages.json
diff --git a/.gitignore b/.gitignore
index ba354d25..394e0172 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
.env
+.env.local
.netlify
.hugo_build.lock
-node_modules
-public
-resources
+node_modules/
+public/
+resources/
+.DS_Store
+.cache/
diff --git a/README.md b/README.md
index 61ec4ed4..fc20117d 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ A **FACODI** é uma plataforma de aprendizagem aberta que replica planos curricu
- 🎓 Unidades curriculares com ementa, resultados de aprendizagem e tópicos
- 🎥 Playlists abertas do YouTube organizadas por prioridade
- 📝 Conteúdo em Markdown sincronizado automaticamente com o Supabase
-- 🌐 Multilíngue (PT como padrão, EN preparado como fallback)
+- 🌐 Multilíngue (PT, EN, ES, FR)
---
@@ -33,15 +33,27 @@ facodi.pt/
├─ config/
│ └─ _default/
├─ content/
-│ ├─ _index.md
-│ └─ courses/
-│ └─ LESTI/
-│ └─ 2024-2025/
-│ ├─ index.md # Curso
-│ └─ uc/
-│ └─ LESTI-ALG1/
-│ ├─ index.md # Unidade curricular
-│ └─ estruturas-de-dados.md # Tópico
+│ ├─ pt/
+│ │ ├─ _index.md
+│ │ └─ courses/
+│ │ └─ LESTI/
+│ │ └─ 2024-2025/
+│ │ ├─ index.md # Curso (PT)
+│ │ └─ uc/
+│ │ └─ LESTI-ALG1/
+│ │ ├─ index.md # Unidade curricular (PT)
+│ │ └─ estruturas-de-dados.md # Tópico (PT)
+│ ├─ en/
+│ │ └─ _index.md
+│ ├─ es/
+│ │ └─ _index.md
+│ └─ fr/
+│ └─ _index.md
+├─ i18n/
+│ ├─ pt.yaml
+│ ├─ en.yaml
+│ ├─ es.yaml
+│ └─ fr.yaml
├─ layouts/
│ ├─ _default/baseof.html
│ ├─ course/single.html
@@ -63,6 +75,14 @@ facodi.pt/
---
+## 🧭 Separação de responsabilidades
+
+- **Hugo (este repositório)**: guarda todo o conteúdo editorial, configurações multilíngues, layouts e scripts que sincronizam Markdown → Supabase. As chaves do Supabase são lidas via variáveis de ambiente (Netlify/GitHub Actions) e nunca ficam em arquivos versionados.
+- **Portal Next.js (futuro)**: consumirá o conteúdo publicado no Supabase para experiências dinâmicas. Quando o portal estiver ativo, chamadas ao Supabase serão feitas no front-end do portal, enquanto o site Hugo permanece estático.
+- **Supabase**: continua a ser a origem de dados sincronizada pelos scripts `lint:content`/`sync:content`, preservando o esquema existente (`catalog`, `subjects`, `mapping`).
+
+---
+
## 🛠️ Scripts disponíveis
| Script | Descrição |
diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml
index 72679145..09ac7205 100644
--- a/config/_default/hugo.toml
+++ b/config/_default/hugo.toml
@@ -10,7 +10,7 @@ rssLimit = 10
summarylength = 40
defaultContentLanguage = "pt"
-defaultContentLanguageInSubdir = false
+defaultContentLanguageInSubdir = true
copyRight = "FACODI — Conteúdo aberto para aprendizagem comunitária"
diff --git a/config/_default/languages.toml b/config/_default/languages.toml
index 8846aa2e..0685daf0 100644
--- a/config/_default/languages.toml
+++ b/config/_default/languages.toml
@@ -1,6 +1,6 @@
[pt]
languageName = "Português"
- contentDir = "content"
+ contentDir = "content/pt"
weight = 1
[pt.params]
languageISO = "PT"
@@ -14,4 +14,22 @@
[en.params]
languageISO = "EN"
languageTag = "en-US"
- footer = 'Built by the FACODI community. English content coming soon.'
+ footer = 'Built by the FACODI community. English content is being translated progressively.'
+
+[es]
+ languageName = "Español"
+ contentDir = "content/es"
+ weight = 3
+ [es.params]
+ languageISO = "ES"
+ languageTag = "es-ES"
+ footer = 'Construido por la comunidad FACODI. Traducciones en curso.'
+
+[fr]
+ languageName = "Français"
+ contentDir = "content/fr"
+ weight = 4
+ [fr.params]
+ languageISO = "FR"
+ languageTag = "fr-FR"
+ footer = 'Construit par la communauté FACODI. Traductions en cours.'
diff --git a/config/_default/menus/menus.es.toml b/config/_default/menus/menus.es.toml
new file mode 100644
index 00000000..3afba7b9
--- /dev/null
+++ b/config/_default/menus/menus.es.toml
@@ -0,0 +1,9 @@
+[[main]]
+ name = "Cursos"
+ url = "/"
+ weight = 10
+
+[[footer]]
+ name = "Comunidad"
+ url = "https://github.com/Monynha-Softwares/facodi.pt"
+ weight = 10
diff --git a/config/_default/menus/menus.fr.toml b/config/_default/menus/menus.fr.toml
new file mode 100644
index 00000000..b43ee009
--- /dev/null
+++ b/config/_default/menus/menus.fr.toml
@@ -0,0 +1,9 @@
+[[main]]
+ name = "Cours"
+ url = "/"
+ weight = 10
+
+[[footer]]
+ name = "Communauté"
+ url = "https://github.com/Monynha-Softwares/facodi.pt"
+ weight = 10
diff --git a/config/_default/params.toml b/config/_default/params.toml
index 1ce8f688..6e1bb80d 100644
--- a/config/_default/params.toml
+++ b/config/_default/params.toml
@@ -63,10 +63,6 @@ mainSections = ["courses"]
krokiURL = "https://kroki.io"
-[supabase]
- url = ""
- anon_key = ""
-
[seo]
[seo.title]
separator = " | "
diff --git a/content/en/_index.md b/content/en/_index.md
index 41521c1f..02fcc0a5 100644
--- a/content/en/_index.md
+++ b/content/en/_index.md
@@ -1,7 +1,7 @@
---
title: "FACODI Catalog"
layout: home
-draft: true
---
FACODI content is primarily authored in Portuguese. English translations will be published as they become available.
+In the meantime, you can explore the Portuguese catalog while we expand our multilingual offering.
diff --git a/content/es/_index.md b/content/es/_index.md
new file mode 100644
index 00000000..c6c3ca65
--- /dev/null
+++ b/content/es/_index.md
@@ -0,0 +1,7 @@
+---
+title: "Catálogo FACODI"
+layout: home
+---
+
+El catálogo original de FACODI se publica en portugués y las traducciones se añaden de forma progresiva.
+Mientras trabajamos en la localización completa, puedes navegar por la versión portuguesa para descubrir los cursos y unidades curriculares disponibles.
diff --git a/content/fr/_index.md b/content/fr/_index.md
new file mode 100644
index 00000000..5fe4bd9f
--- /dev/null
+++ b/content/fr/_index.md
@@ -0,0 +1,7 @@
+---
+title: "Catalogue FACODI"
+layout: home
+---
+
+Le catalogue principal de FACODI est publié en portugais et les traductions sont ajoutées progressivement.
+En attendant la localisation complète, consultez la version portugaise pour explorer les cours et les unités curriculaires disponibles.
diff --git a/content/_index.md b/content/pt/_index.md
similarity index 100%
rename from content/_index.md
rename to content/pt/_index.md
diff --git a/content/courses/LESTI/2024-2025/index.md b/content/pt/courses/LESTI/2024-2025/index.md
similarity index 100%
rename from content/courses/LESTI/2024-2025/index.md
rename to content/pt/courses/LESTI/2024-2025/index.md
diff --git a/content/courses/LESTI/2024-2025/uc/LESTI-ALG1/estruturas-de-dados.md b/content/pt/courses/LESTI/2024-2025/uc/LESTI-ALG1/estruturas-de-dados.md
similarity index 100%
rename from content/courses/LESTI/2024-2025/uc/LESTI-ALG1/estruturas-de-dados.md
rename to content/pt/courses/LESTI/2024-2025/uc/LESTI-ALG1/estruturas-de-dados.md
diff --git a/content/courses/LESTI/2024-2025/uc/LESTI-ALG1/index.md b/content/pt/courses/LESTI/2024-2025/uc/LESTI-ALG1/index.md
similarity index 100%
rename from content/courses/LESTI/2024-2025/uc/LESTI-ALG1/index.md
rename to content/pt/courses/LESTI/2024-2025/uc/LESTI-ALG1/index.md
diff --git a/hugo_stats.json b/hugo_stats.json
index 5176c09e..bbb4761e 100644
--- a/hugo_stats.json
+++ b/hugo_stats.json
@@ -23,7 +23,6 @@
"line",
"link",
"meta",
- "nav",
"noscript",
"p",
"path",
@@ -58,14 +57,12 @@
"container",
"container-lg",
"content",
- "courses",
"current",
"d-flex",
"d-inline-flex",
"d-lg-none",
"display-5",
"display-6",
- "doks-sidebar",
"dropdown",
"dropdown-caret",
"dropdown-divider",
@@ -73,12 +70,15 @@
"dropdown-menu",
"dropdown-menu-lg-end",
"dropdown-toggle",
+ "en",
"error404",
+ "es",
"fixed-bottom-right",
"flex-column",
"flex-grow-1",
"flex-lg-row",
"footer",
+ "fr",
"fw-semibold",
"g-3",
"g-4",
@@ -92,7 +92,6 @@
"icon-tabler",
"icon-tabler-chevron-down",
"icon-tabler-chevron-up",
- "icon-tabler-dots-vertical",
"icon-tabler-language",
"icon-tabler-menu",
"icon-tabler-moon",
@@ -100,7 +99,6 @@
"icon-tabler-x",
"justify-content-between",
"justify-content-center",
- "lead",
"list",
"list-inline",
"list-inline-item",
@@ -117,8 +115,6 @@
"ms-auto",
"ms-lg-2",
"mt-1",
- "mt-3",
- "mt-n3",
"mx-2",
"mx-auto",
"my-3",
@@ -132,7 +128,6 @@
"offcanvas-body",
"offcanvas-end",
"offcanvas-header",
- "offcanvas-start",
"offcanvas-title",
"order-3",
"order-lg-2",
@@ -141,6 +136,7 @@
"p-4",
"pb-4",
"pe-4",
+ "pt",
"py-5",
"rounded",
"rounded-3",
@@ -160,7 +156,6 @@
"text-lg-end",
"text-lg-start",
"text-muted",
- "text-start",
"text-uppercase",
"visually-hidden",
"wrap"
@@ -168,13 +163,10 @@
"ids": [
"buttonColorMode",
"course-content-md",
- "doks-docs-nav",
"doks-language-current",
"doks-languages",
"offcanvasNavMain",
"offcanvasNavMainLabel",
- "offcanvasNavSection",
- "offcanvasNavSectionLabel",
"toTop",
"visão-geral"
]
diff --git a/i18n/en.yaml b/i18n/en.yaml
new file mode 100644
index 00000000..5f35c574
--- /dev/null
+++ b/i18n/en.yaml
@@ -0,0 +1,145 @@
+- id: home_plan_label
+ translation: "Plan"
+- id: home_duration_semester
+ translation: "semester"
+- id: home_duration_semesters
+ translation: "semesters"
+- id: home_summary_placeholder
+ translation: "Documentation in progress."
+- id: home_empty_state
+ translation: "No courses published yet. Check back soon!"
+
+- id: course_code_prefix
+ translation: "Course"
+- id: course_plan_label
+ translation: "Curriculum plan"
+- id: course_synced_heading
+ translation: "Synced content"
+- id: course_synced_loading
+ translation: "Loading detailed description from Supabase…"
+- id: course_content_placeholder
+ translation: "No synced content is available for this course yet."
+- id: course_ucs_heading
+ translation: "Course units"
+- id: course_ucs_loading
+ translation: "Loading course units…"
+- id: course_ucs_empty
+ translation: "No course units have been synchronised."
+- id: course_ucs_description_placeholder
+ translation: "Description coming soon."
+- id: course_details_heading
+ translation: "Course details"
+- id: course_details_institution
+ translation: "Institution"
+- id: course_details_school
+ translation: "School"
+- id: course_details_ects
+ translation: "ECTS"
+- id: course_details_duration
+ translation: "Duration"
+- id: course_details_duration_suffix
+ translation: "semesters"
+- id: course_details_duration_suffix_singular
+ translation: "semester"
+- id: course_details_language
+ translation: "Language"
+- id: course_ucs_meta
+ translation: "Semester {{ .semester }} · {{ .ects }} ECTS"
+- id: course_error_details
+ translation: "Failed to load course details."
+- id: course_error_content
+ translation: "We could not load the synced content."
+- id: course_error_ucs
+ translation: "We could not load the course units."
+- id: course_not_found
+ translation: "Course not found."
+
+- id: uc_code_prefix
+ translation: "Course unit"
+- id: uc_synced_heading
+ translation: "Official content"
+- id: uc_synced_loading
+ translation: "Synchronising syllabus and materials…"
+- id: uc_content_placeholder
+ translation: "Content not available yet."
+- id: uc_outcomes_heading
+ translation: "Learning outcomes"
+- id: uc_outcomes_loading
+ translation: "Loading outcomes…"
+- id: uc_outcomes_empty
+ translation: "No outcomes have been registered."
+- id: uc_playlists_heading
+ translation: "Recommended playlists"
+- id: uc_playlists_loading
+ translation: "Loading playlists…"
+- id: uc_playlists_empty
+ translation: "No playlists linked yet."
+- id: uc_playlists_priority_label
+ translation: "Priority"
+- id: uc_playlists_button_label
+ translation: "Open playlist"
+- id: uc_topics_heading
+ translation: "Related topics"
+- id: uc_topics_loading
+ translation: "Loading topics…"
+- id: uc_topics_empty
+ translation: "No topics linked yet."
+- id: uc_topics_summary_placeholder
+ translation: "Summary coming soon."
+- id: uc_details_heading
+ translation: "Information"
+- id: uc_details_course
+ translation: "Course"
+- id: uc_details_ects
+ translation: "ECTS"
+- id: uc_details_semester
+ translation: "Semester"
+- id: uc_details_language
+ translation: "Language"
+- id: uc_details_prerequisites
+ translation: "Prerequisites"
+- id: uc_error_details
+ translation: "Failed to load course-unit information."
+- id: uc_error_content
+ translation: "The synced syllabus could not be loaded."
+- id: uc_error_outcomes
+ translation: "Error loading learning outcomes."
+- id: uc_error_playlists
+ translation: "Error loading playlists."
+- id: uc_error_topics
+ translation: "Error loading topics."
+- id: uc_not_found
+ translation: "Course unit not found."
+
+- id: topic_code_prefix
+ translation: "Topic"
+- id: topic_synced_heading
+ translation: "Synced content"
+- id: topic_synced_loading
+ translation: "Loading Markdown content…"
+- id: topic_content_placeholder
+ translation: "Content coming soon."
+- id: topic_playlists_heading
+ translation: "Related playlists"
+- id: topic_playlists_loading
+ translation: "Loading playlists…"
+- id: topic_playlists_empty
+ translation: "No playlists registered yet."
+- id: topic_playlists_priority_label
+ translation: "Priority"
+- id: topic_playlists_button_label
+ translation: "Open playlist"
+- id: topic_tags_heading
+ translation: "Tags"
+- id: topic_tags_loading
+ translation: "Loading tags…"
+- id: topic_tags_empty
+ translation: "No tags available."
+- id: topic_error_content
+ translation: "The topic content could not be loaded."
+- id: topic_error_playlists
+ translation: "Error loading playlists."
+- id: topic_error_tags
+ translation: "Error loading tags."
+- id: topic_not_found
+ translation: "Topic not found."
diff --git a/i18n/es.yaml b/i18n/es.yaml
new file mode 100644
index 00000000..bf8e8771
--- /dev/null
+++ b/i18n/es.yaml
@@ -0,0 +1,145 @@
+- id: home_plan_label
+ translation: "Plan"
+- id: home_duration_semester
+ translation: "semestre"
+- id: home_duration_semesters
+ translation: "semestres"
+- id: home_summary_placeholder
+ translation: "Documentación en preparación."
+- id: home_empty_state
+ translation: "Todavía no hay cursos publicados. ¡Vuelve pronto!"
+
+- id: course_code_prefix
+ translation: "Curso"
+- id: course_plan_label
+ translation: "Plan curricular"
+- id: course_synced_heading
+ translation: "Contenido sincronizado"
+- id: course_synced_loading
+ translation: "Cargando la descripción detallada desde Supabase…"
+- id: course_content_placeholder
+ translation: "Aún no hay contenido sincronizado para este curso."
+- id: course_ucs_heading
+ translation: "Unidades curriculares"
+- id: course_ucs_loading
+ translation: "Cargando unidades curriculares…"
+- id: course_ucs_empty
+ translation: "No hay unidades curriculares sincronizadas."
+- id: course_ucs_description_placeholder
+ translation: "Descripción en preparación."
+- id: course_details_heading
+ translation: "Detalles del curso"
+- id: course_details_institution
+ translation: "Institución"
+- id: course_details_school
+ translation: "Escuela"
+- id: course_details_ects
+ translation: "ECTS"
+- id: course_details_duration
+ translation: "Duración"
+- id: course_details_duration_suffix
+ translation: "semestres"
+- id: course_details_duration_suffix_singular
+ translation: "semestre"
+- id: course_details_language
+ translation: "Idioma"
+- id: course_ucs_meta
+ translation: "Semestre {{ .semester }} · {{ .ects }} ECTS"
+- id: course_error_details
+ translation: "Error al cargar los detalles del curso."
+- id: course_error_content
+ translation: "No fue posible cargar el contenido sincronizado."
+- id: course_error_ucs
+ translation: "No fue posible cargar las unidades curriculares."
+- id: course_not_found
+ translation: "Curso no encontrado."
+
+- id: uc_code_prefix
+ translation: "Unidad curricular"
+- id: uc_synced_heading
+ translation: "Contenido oficial"
+- id: uc_synced_loading
+ translation: "Sincronizando programa y materiales…"
+- id: uc_content_placeholder
+ translation: "Contenido aún no disponible."
+- id: uc_outcomes_heading
+ translation: "Resultados de aprendizaje"
+- id: uc_outcomes_loading
+ translation: "Cargando resultados…"
+- id: uc_outcomes_empty
+ translation: "No hay resultados registrados."
+- id: uc_playlists_heading
+ translation: "Listas recomendadas"
+- id: uc_playlists_loading
+ translation: "Cargando listas…"
+- id: uc_playlists_empty
+ translation: "No hay listas asociadas."
+- id: uc_playlists_priority_label
+ translation: "Prioridad"
+- id: uc_playlists_button_label
+ translation: "Abrir lista"
+- id: uc_topics_heading
+ translation: "Temas asociados"
+- id: uc_topics_loading
+ translation: "Cargando temas…"
+- id: uc_topics_empty
+ translation: "No hay temas asociados."
+- id: uc_topics_summary_placeholder
+ translation: "Resumen en preparación."
+- id: uc_details_heading
+ translation: "Información"
+- id: uc_details_course
+ translation: "Curso"
+- id: uc_details_ects
+ translation: "ECTS"
+- id: uc_details_semester
+ translation: "Semestre"
+- id: uc_details_language
+ translation: "Idioma"
+- id: uc_details_prerequisites
+ translation: "Prerrequisitos"
+- id: uc_error_details
+ translation: "Error al cargar la información de la unidad curricular."
+- id: uc_error_content
+ translation: "No fue posible cargar el programa sincronizado."
+- id: uc_error_outcomes
+ translation: "Error al cargar los resultados de aprendizaje."
+- id: uc_error_playlists
+ translation: "Error al cargar las listas."
+- id: uc_error_topics
+ translation: "Error al cargar los temas."
+- id: uc_not_found
+ translation: "Unidad curricular no encontrada."
+
+- id: topic_code_prefix
+ translation: "Tema"
+- id: topic_synced_heading
+ translation: "Contenido sincronizado"
+- id: topic_synced_loading
+ translation: "Cargando contenido Markdown…"
+- id: topic_content_placeholder
+ translation: "Contenido en preparación."
+- id: topic_playlists_heading
+ translation: "Listas relacionadas"
+- id: topic_playlists_loading
+ translation: "Cargando listas…"
+- id: topic_playlists_empty
+ translation: "No hay listas registradas."
+- id: topic_playlists_priority_label
+ translation: "Prioridad"
+- id: topic_playlists_button_label
+ translation: "Abrir lista"
+- id: topic_tags_heading
+ translation: "Etiquetas"
+- id: topic_tags_loading
+ translation: "Cargando etiquetas…"
+- id: topic_tags_empty
+ translation: "No hay etiquetas."
+- id: topic_error_content
+ translation: "No fue posible cargar el contenido del tema."
+- id: topic_error_playlists
+ translation: "Error al cargar las listas."
+- id: topic_error_tags
+ translation: "Error al cargar las etiquetas."
+- id: topic_not_found
+ translation: "Tema no encontrado."
diff --git a/i18n/fr.yaml b/i18n/fr.yaml
new file mode 100644
index 00000000..320b8858
--- /dev/null
+++ b/i18n/fr.yaml
@@ -0,0 +1,145 @@
+- id: home_plan_label
+ translation: "Plan"
+- id: home_duration_semester
+ translation: "semestre"
+- id: home_duration_semesters
+ translation: "semestres"
+- id: home_summary_placeholder
+ translation: "Documentation en préparation."
+- id: home_empty_state
+ translation: "Aucun cours publié pour le moment. Revenez bientôt !"
+
+- id: course_code_prefix
+ translation: "Cours"
+- id: course_plan_label
+ translation: "Plan d'études"
+- id: course_synced_heading
+ translation: "Contenu synchronisé"
+- id: course_synced_loading
+ translation: "Chargement de la description détaillée depuis Supabase…"
+- id: course_content_placeholder
+ translation: "Aucun contenu synchronisé n'est disponible pour ce cours pour l'instant."
+- id: course_ucs_heading
+ translation: "Unités curriculaires"
+- id: course_ucs_loading
+ translation: "Chargement des unités curriculaires…"
+- id: course_ucs_empty
+ translation: "Aucune unité curriculaire synchronisée."
+- id: course_ucs_description_placeholder
+ translation: "Description en préparation."
+- id: course_details_heading
+ translation: "Détails du cours"
+- id: course_details_institution
+ translation: "Institution"
+- id: course_details_school
+ translation: "École"
+- id: course_details_ects
+ translation: "ECTS"
+- id: course_details_duration
+ translation: "Durée"
+- id: course_details_duration_suffix
+ translation: "semestres"
+- id: course_details_duration_suffix_singular
+ translation: "semestre"
+- id: course_details_language
+ translation: "Langue"
+- id: course_ucs_meta
+ translation: "Semestre {{ .semester }} · {{ .ects }} ECTS"
+- id: course_error_details
+ translation: "Échec du chargement des détails du cours."
+- id: course_error_content
+ translation: "Impossible de charger le contenu synchronisé."
+- id: course_error_ucs
+ translation: "Impossible de charger les unités curriculaires."
+- id: course_not_found
+ translation: "Cours introuvable."
+
+- id: uc_code_prefix
+ translation: "Unité curriculaire"
+- id: uc_synced_heading
+ translation: "Contenu officiel"
+- id: uc_synced_loading
+ translation: "Synchronisation du programme et des ressources…"
+- id: uc_content_placeholder
+ translation: "Contenu non disponible pour le moment."
+- id: uc_outcomes_heading
+ translation: "Résultats d'apprentissage"
+- id: uc_outcomes_loading
+ translation: "Chargement des résultats…"
+- id: uc_outcomes_empty
+ translation: "Aucun résultat enregistré."
+- id: uc_playlists_heading
+ translation: "Playlists recommandées"
+- id: uc_playlists_loading
+ translation: "Chargement des playlists…"
+- id: uc_playlists_empty
+ translation: "Aucune playlist associée."
+- id: uc_playlists_priority_label
+ translation: "Priorité"
+- id: uc_playlists_button_label
+ translation: "Ouvrir la playlist"
+- id: uc_topics_heading
+ translation: "Sujets associés"
+- id: uc_topics_loading
+ translation: "Chargement des sujets…"
+- id: uc_topics_empty
+ translation: "Aucun sujet associé."
+- id: uc_topics_summary_placeholder
+ translation: "Résumé en préparation."
+- id: uc_details_heading
+ translation: "Informations"
+- id: uc_details_course
+ translation: "Cours"
+- id: uc_details_ects
+ translation: "ECTS"
+- id: uc_details_semester
+ translation: "Semestre"
+- id: uc_details_language
+ translation: "Langue"
+- id: uc_details_prerequisites
+ translation: "Prérequis"
+- id: uc_error_details
+ translation: "Échec du chargement des informations de l'unité curriculaire."
+- id: uc_error_content
+ translation: "Impossible de charger le programme synchronisé."
+- id: uc_error_outcomes
+ translation: "Erreur lors du chargement des résultats d'apprentissage."
+- id: uc_error_playlists
+ translation: "Erreur lors du chargement des playlists."
+- id: uc_error_topics
+ translation: "Erreur lors du chargement des sujets."
+- id: uc_not_found
+ translation: "Unité curriculaire introuvable."
+
+- id: topic_code_prefix
+ translation: "Sujet"
+- id: topic_synced_heading
+ translation: "Contenu synchronisé"
+- id: topic_synced_loading
+ translation: "Chargement du contenu Markdown…"
+- id: topic_content_placeholder
+ translation: "Contenu en préparation."
+- id: topic_playlists_heading
+ translation: "Playlists associées"
+- id: topic_playlists_loading
+ translation: "Chargement des playlists…"
+- id: topic_playlists_empty
+ translation: "Aucune playlist enregistrée."
+- id: topic_playlists_priority_label
+ translation: "Priorité"
+- id: topic_playlists_button_label
+ translation: "Ouvrir la playlist"
+- id: topic_tags_heading
+ translation: "Étiquettes"
+- id: topic_tags_loading
+ translation: "Chargement des étiquettes…"
+- id: topic_tags_empty
+ translation: "Aucune étiquette."
+- id: topic_error_content
+ translation: "Impossible de charger le contenu du sujet."
+- id: topic_error_playlists
+ translation: "Erreur lors du chargement des playlists."
+- id: topic_error_tags
+ translation: "Erreur lors du chargement des étiquettes."
+- id: topic_not_found
+ translation: "Sujet introuvable."
diff --git a/i18n/pt.yaml b/i18n/pt.yaml
new file mode 100644
index 00000000..51b6914f
--- /dev/null
+++ b/i18n/pt.yaml
@@ -0,0 +1,145 @@
+- id: home_plan_label
+ translation: "Plano"
+- id: home_duration_semester
+ translation: "semestre"
+- id: home_duration_semesters
+ translation: "semestres"
+- id: home_summary_placeholder
+ translation: "Documentação em preparação."
+- id: home_empty_state
+ translation: "Nenhum curso publicado ainda. Volte em breve!"
+
+- id: course_code_prefix
+ translation: "Curso"
+- id: course_plan_label
+ translation: "Plano curricular"
+- id: course_synced_heading
+ translation: "Conteúdo sincronizado"
+- id: course_synced_loading
+ translation: "Carregando descrição detalhada a partir do Supabase…"
+- id: course_content_placeholder
+ translation: "Ainda não há conteúdo sincronizado para este curso."
+- id: course_ucs_heading
+ translation: "Unidades curriculares"
+- id: course_ucs_loading
+ translation: "Carregando unidades curriculares…"
+- id: course_ucs_empty
+ translation: "Nenhuma unidade curricular sincronizada."
+- id: course_ucs_description_placeholder
+ translation: "Descrição em preparação."
+- id: course_details_heading
+ translation: "Detalhes do curso"
+- id: course_details_institution
+ translation: "Instituição"
+- id: course_details_school
+ translation: "Escola"
+- id: course_details_ects
+ translation: "ECTS"
+- id: course_details_duration
+ translation: "Duração"
+- id: course_details_duration_suffix
+ translation: "semestres"
+- id: course_details_duration_suffix_singular
+ translation: "semestre"
+- id: course_details_language
+ translation: "Idioma"
+- id: course_ucs_meta
+ translation: "{{ .semester }}º semestre · {{ .ects }} ECTS"
+- id: course_error_details
+ translation: "Falha ao carregar detalhes do curso."
+- id: course_error_content
+ translation: "Não foi possível carregar o conteúdo sincronizado."
+- id: course_error_ucs
+ translation: "Não foi possível carregar as unidades curriculares."
+- id: course_not_found
+ translation: "Curso não encontrado."
+
+- id: uc_code_prefix
+ translation: "Unidade curricular"
+- id: uc_synced_heading
+ translation: "Conteúdo oficial"
+- id: uc_synced_loading
+ translation: "Sincronizando ementa e materiais…"
+- id: uc_content_placeholder
+ translation: "Conteúdo ainda não disponível."
+- id: uc_outcomes_heading
+ translation: "Resultados de aprendizagem"
+- id: uc_outcomes_loading
+ translation: "Carregando resultados…"
+- id: uc_outcomes_empty
+ translation: "Nenhum resultado cadastrado."
+- id: uc_playlists_heading
+ translation: "Playlists recomendadas"
+- id: uc_playlists_loading
+ translation: "Carregando playlists…"
+- id: uc_playlists_empty
+ translation: "Sem playlists associadas."
+- id: uc_playlists_priority_label
+ translation: "Prioridade"
+- id: uc_playlists_button_label
+ translation: "Abrir playlist"
+- id: uc_topics_heading
+ translation: "Tópicos associados"
+- id: uc_topics_loading
+ translation: "Carregando tópicos…"
+- id: uc_topics_empty
+ translation: "Sem tópicos associados."
+- id: uc_topics_summary_placeholder
+ translation: "Resumo em preparação."
+- id: uc_details_heading
+ translation: "Informações"
+- id: uc_details_course
+ translation: "Curso"
+- id: uc_details_ects
+ translation: "ECTS"
+- id: uc_details_semester
+ translation: "Semestre"
+- id: uc_details_language
+ translation: "Idioma"
+- id: uc_details_prerequisites
+ translation: "Pré-requisitos"
+- id: uc_error_details
+ translation: "Falha ao carregar informações da UC."
+- id: uc_error_content
+ translation: "Não foi possível carregar a ementa sincronizada."
+- id: uc_error_outcomes
+ translation: "Erro ao carregar resultados de aprendizagem."
+- id: uc_error_playlists
+ translation: "Erro ao carregar playlists."
+- id: uc_error_topics
+ translation: "Erro ao carregar tópicos."
+- id: uc_not_found
+ translation: "UC não encontrada."
+
+- id: topic_code_prefix
+ translation: "Tópico"
+- id: topic_synced_heading
+ translation: "Conteúdo sincronizado"
+- id: topic_synced_loading
+ translation: "Carregando conteúdo Markdown…"
+- id: topic_content_placeholder
+ translation: "Conteúdo em preparação."
+- id: topic_playlists_heading
+ translation: "Playlists relacionadas"
+- id: topic_playlists_loading
+ translation: "Carregando playlists…"
+- id: topic_playlists_empty
+ translation: "Sem playlists cadastradas."
+- id: topic_playlists_priority_label
+ translation: "Prioridade"
+- id: topic_playlists_button_label
+ translation: "Abrir playlist"
+- id: topic_tags_heading
+ translation: "Etiquetas"
+- id: topic_tags_loading
+ translation: "Carregando tags…"
+- id: topic_tags_empty
+ translation: "Sem etiquetas."
+- id: topic_error_content
+ translation: "Não foi possível carregar o conteúdo do tópico."
+- id: topic_error_playlists
+ translation: "Erro ao carregar playlists."
+- id: topic_error_tags
+ translation: "Erro ao carregar tags."
+- id: topic_not_found
+ translation: "Tópico não encontrado."
diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html
index a98637f9..e5594b5a 100644
--- a/layouts/_default/baseof.html
+++ b/layouts/_default/baseof.html
@@ -2,7 +2,14 @@
{{ partial "head/head" . }}
{{ partial "head/body-class" . }}
-
+ {{- $supabaseUrl := getenv "SUPABASE_URL" -}}
+ {{- $supabaseAnon := getenv "SUPABASE_ANON_KEY" -}}
+ {{- $messagesJSON := partial "i18n/messages.json" . -}}
+ {{- $messagesEncoded := "" -}}
+ {{- if gt (len $messagesJSON) 0 -}}
+ {{- $messagesEncoded = base64Encode $messagesJSON -}}
+ {{- end -}}
+
{{ partial "header/header" . }}
diff --git a/layouts/_partials/footer/script-footer-custom.html b/layouts/_partials/footer/script-footer-custom.html
index 9a1de999..be9ac844 100644
--- a/layouts/_partials/footer/script-footer-custom.html
+++ b/layouts/_partials/footer/script-footer-custom.html
@@ -1,9 +1,3 @@
-{{- $supabaseUrl := site.Params.supabase.url | default (getenv "SUPABASE_URL") -}}
-{{- $supabaseAnon := site.Params.supabase.anon_key | default (getenv "SUPABASE_ANON_KEY") -}}
-
diff --git a/layouts/_partials/i18n/messages.json b/layouts/_partials/i18n/messages.json
new file mode 100644
index 00000000..da27c3e3
--- /dev/null
+++ b/layouts/_partials/i18n/messages.json
@@ -0,0 +1,24 @@
+{{- $courseDetails := dict "heading" (i18n "course_details_heading") "institution" (i18n "course_details_institution") "school" (i18n "course_details_school") "ects" (i18n "course_details_ects") "duration" (i18n "course_details_duration") "durationSuffixSingular" (i18n "course_details_duration_suffix_singular") "durationSuffixPlural" (i18n "course_details_duration_suffix") "language" (i18n "course_details_language") -}}
+{{- $courseLoading := dict "content" (i18n "course_synced_loading") "ucs" (i18n "course_ucs_loading") -}}
+{{- $courseEmpty := dict "content" (i18n "course_content_placeholder") "ucs" (i18n "course_ucs_empty") "ucDescription" (i18n "course_ucs_description_placeholder") -}}
+{{- $courseErrors := dict "details" (i18n "course_error_details") "content" (i18n "course_error_content") "ucs" (i18n "course_error_ucs") "notFound" (i18n "course_not_found") -}}
+{{- $courseMeta := dict "uc" (i18n "course_ucs_meta") -}}
+
+{{- $ucDetails := dict "heading" (i18n "uc_details_heading") "course" (i18n "uc_details_course") "ects" (i18n "uc_details_ects") "semester" (i18n "uc_details_semester") "language" (i18n "uc_details_language") "prerequisites" (i18n "uc_details_prerequisites") -}}
+{{- $ucLoading := dict "content" (i18n "uc_synced_loading") "outcomes" (i18n "uc_outcomes_loading") "playlists" (i18n "uc_playlists_loading") "topics" (i18n "uc_topics_loading") -}}
+{{- $ucEmpty := dict "outcomes" (i18n "uc_outcomes_empty") "playlists" (i18n "uc_playlists_empty") "topics" (i18n "uc_topics_empty") -}}
+{{- $ucPlaceholders := dict "content" (i18n "uc_content_placeholder") "summary" (i18n "uc_topics_summary_placeholder") -}}
+{{- $ucLabels := dict "priority" (i18n "uc_playlists_priority_label") -}}
+{{- $ucButtons := dict "openPlaylist" (i18n "uc_playlists_button_label") -}}
+{{- $ucErrors := dict "details" (i18n "uc_error_details") "content" (i18n "uc_error_content") "outcomes" (i18n "uc_error_outcomes") "playlists" (i18n "uc_error_playlists") "topics" (i18n "uc_error_topics") "notFound" (i18n "uc_not_found") -}}
+
+{{- $topicLoading := dict "content" (i18n "topic_synced_loading") "playlists" (i18n "topic_playlists_loading") "tags" (i18n "topic_tags_loading") -}}
+{{- $topicEmpty := dict "playlists" (i18n "topic_playlists_empty") "tags" (i18n "topic_tags_empty") -}}
+{{- $topicPlaceholders := dict "content" (i18n "topic_content_placeholder") -}}
+{{- $topicLabels := dict "priority" (i18n "topic_playlists_priority_label") -}}
+{{- $topicButtons := dict "openPlaylist" (i18n "topic_playlists_button_label") -}}
+{{- $topicErrors := dict "content" (i18n "topic_error_content") "playlists" (i18n "topic_error_playlists") "tags" (i18n "topic_error_tags") "notFound" (i18n "topic_not_found") -}}
+
+{{- $messages := dict "course" (dict "details" $courseDetails "loading" $courseLoading "empty" $courseEmpty "errors" $courseErrors "meta" $courseMeta) "uc" (dict "details" $ucDetails "loading" $ucLoading "empty" $ucEmpty "placeholders" $ucPlaceholders "labels" $ucLabels "buttons" $ucButtons "errors" $ucErrors) "topic" (dict "loading" $topicLoading "empty" $topicEmpty "placeholders" $topicPlaceholders "labels" $topicLabels "buttons" $topicButtons "errors" $topicErrors) -}}
+
+{{- $messages | jsonify -}}
diff --git a/layouts/course/single.html b/layouts/course/single.html
index b2b9844f..ee35aab5 100644
--- a/layouts/course/single.html
+++ b/layouts/course/single.html
@@ -3,42 +3,42 @@
{{ .Content }}
- Conteúdo sincronizado
+ {{ i18n "course_synced_heading" }}
-
Carregando descrição detalhada a partir do Supabase…
+
{{ i18n "course_synced_loading" }}
- Unidades curriculares
+ {{ i18n "course_ucs_heading" }}
-
Carregando unidades curriculares…
+
{{ i18n "course_ucs_loading" }}
@@ -41,7 +46,7 @@
{{ end }}
{{ else }}
-
Nenhum curso publicado ainda. Volte em breve!
+
{{ i18n "home_empty_state" }}
{{ end }}
diff --git a/layouts/topic/single.html b/layouts/topic/single.html
index dabb4d43..2d79a1b9 100644
--- a/layouts/topic/single.html
+++ b/layouts/topic/single.html
@@ -3,28 +3,28 @@
{{ .Content }}
- Conteúdo sincronizado
+ {{ i18n "topic_synced_heading" }}
-
Carregando conteúdo Markdown…
+
{{ i18n "topic_synced_loading" }}
- Playlists relacionadas
+ {{ i18n "topic_playlists_heading" }}
-
Carregando playlists…
+
{{ i18n "topic_playlists_loading" }}
- Etiquetas
+ {{ i18n "topic_tags_heading" }}
- Carregando tags…
+ {{ i18n "topic_tags_loading" }}
diff --git a/layouts/uc/single.html b/layouts/uc/single.html
index 4036e83c..ee75d812 100644
--- a/layouts/uc/single.html
+++ b/layouts/uc/single.html
@@ -3,51 +3,51 @@
{{ .Content }}
- Conteúdo oficial
+ {{ i18n "uc_synced_heading" }}
-
Sincronizando ementa e materiais…
+
{{ i18n "uc_synced_loading" }}
- Resultados de aprendizagem
+ {{ i18n "uc_outcomes_heading" }}
- - Carregando resultados…
+ - {{ i18n "uc_outcomes_loading" }}
- Playlists recomendadas
+ {{ i18n "uc_playlists_heading" }}
-
Carregando playlists…
+
{{ i18n "uc_playlists_loading" }}
- Tópicos associados
+ {{ i18n "uc_topics_heading" }}
-
Carregando tópicos…
+
{{ i18n "uc_topics_loading" }}