feat: implement dynamic news pages and listing components with internationalization support

This commit is contained in:
Esteban Paz 2026-05-07 20:50:53 -05:00
parent 5731cf245d
commit aa54417618
10 changed files with 163 additions and 107 deletions

View File

@ -5,6 +5,7 @@ import "dayjs/locale/es";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
const regionNames = new Intl.DisplayNames(['es'], { type: 'region' }); const regionNames = new Intl.DisplayNames(['es'], { type: 'region' });
import { getLocalizedRoute } from "../../i18n";
const locale = Astro.currentLocale; const locale = Astro.currentLocale;
dayjs.extend(utc); dayjs.extend(utc);
@ -24,7 +25,7 @@ locationArray.filter(Boolean).join(', ');
{locationArray.filter(Boolean).join(', ')}<br /> {locationArray.filter(Boolean).join(', ')}<br />
({nicedate}): ({nicedate}):
</p> </p>
<h3 class="text-2xl font-bold mb-8"><a href={`/${locale}/news/${data.id}`}>{data.data.title}</a></h3> <h3 class="text-2xl font-bold mb-8"><a href={`/${locale}/${getLocalizedRoute('news', locale)}/${data.id}`}>{data.data.title}</a></h3>
<div> <div>
<Image <Image

View File

@ -7,7 +7,7 @@ import utc from "dayjs/plugin/utc";
const regionNames = new Intl.DisplayNames(["es"], { type: "region" }); const regionNames = new Intl.DisplayNames(["es"], { type: "region" });
const locale = Astro.currentLocale; const locale = Astro.currentLocale;
import { createTranslator } from "@/i18n"; import { createTranslator, getLocalizedRoute } from "@/i18n";
const tl = createTranslator(Astro.currentLocale); const tl = createTranslator(Astro.currentLocale);
dayjs.extend(utc); dayjs.extend(utc);
dayjs.locale(locale); dayjs.locale(locale);
@ -21,7 +21,7 @@ const countryName = data?.data?.country
const locationArray = [data.data.city, data.data.state, countryName]; const locationArray = [data.data.city, data.data.state, countryName];
const location = locationArray.filter(Boolean).join(", "); const location = locationArray.filter(Boolean).join(", ");
const newsUrl = `/${locale}/news/${data.id}`; const newsUrl = `/${locale}/${getLocalizedRoute('news', locale)}/${data.id}`;
const rawContent = content?.body || ""; const rawContent = content?.body || "";
const plainText = rawContent const plainText = rawContent

View File

@ -9,8 +9,7 @@ const newsItems = await getCollection("news", (post)=>{
const currentLocale = Astro.currentLocale; const currentLocale = Astro.currentLocale;
return post.data.locale == currentLocale return post.data.locale == currentLocale
}); });
import { createTranslator, getLocalizedRoute } from '../../i18n';
import { createTranslator, t } from '../../i18n';
const tl = createTranslator(Astro.currentLocale); const tl = createTranslator(Astro.currentLocale);
--- ---
<div id="news" class="bg-[#22523F] py-12 lg:py-20"> <div id="news" class="bg-[#22523F] py-12 lg:py-20">
@ -19,7 +18,7 @@ const tl = createTranslator(Astro.currentLocale);
<h4 class="text-white text-2xl uppercase font-bold text-center mb-4 font-primary">{tl("news.title")}</h4> <h4 class="text-white text-2xl uppercase font-bold text-center mb-4 font-primary">{tl("news.title")}</h4>
<h2 class="text-white text-3xl lg:text-5xl font-bold text-center font-secondary mb-4">{tl("news.text")}</h2> <h2 class="text-white text-3xl lg:text-5xl font-bold text-center font-secondary mb-4">{tl("news.text")}</h2>
<p class="text-white text-xl text-center">{tl("news.text2")}</p> <p class="text-white text-xl text-center">{tl("news.text2")}</p>
<Button class="px-6 py-3 uppercase mt-4" url=`/${currentLocale}/news` variant="primary" title={tl("news.buttonLable")} /> <Button class="px-6 py-3 uppercase mt-4" url={`/${currentLocale}/${getLocalizedRoute('news', currentLocale)}`} variant="primary" title={tl("news.buttonLable")} />
</div> </div>

View File

@ -11,9 +11,27 @@ import kr from "./kr.json";
const dictionaries = { es, en, fr, he, uk, pt, ru, rw, kr } as const; const dictionaries = { es, en, fr, he, uk, pt, ru, rw, kr } as const;
export type Locale = keyof typeof dictionaries; export type Locale = keyof typeof dictionaries;
// Optional: type-safe keys from the default dictionary
export type I18nKey = keyof typeof es; export type I18nKey = keyof typeof es;
export const routeTranslations = {
news: {
es: "noticias",
en: "news",
fr: "informations",
he: "חדשות",
uk: "noticias",
pt: "noticias",
ru: "новости",
rw: "amakuru",
kr: "nouvel",
}
} as const;
export function getLocalizedRoute(route: keyof typeof routeTranslations, locale: string | undefined): string {
const l = (locale in routeTranslations[route] ? locale : "es") as Locale;
return routeTranslations[route][l as keyof (typeof routeTranslations)[typeof route]] || routeTranslations[route]["en" as keyof (typeof routeTranslations)[typeof route]];
}
export function t( export function t(
locale: string | undefined, locale: string | undefined,
key: I18nKey, key: I18nKey,

View File

@ -8,6 +8,7 @@ import "@fontsource/poppins/500.css";
import "@fontsource/poppins/700.css"; import "@fontsource/poppins/700.css";
import "@fontsource-variable/kameron"; import "@fontsource-variable/kameron";
import ShareSticky from "../components/ShareSticky.vue"; import ShareSticky from "../components/ShareSticky.vue";
import { routeTranslations } from "../i18n";
const { const {
title, title,
description, description,
@ -17,6 +18,9 @@ const {
const currentLocale = Astro.currentLocale ?? 'es'; const currentLocale = Astro.currentLocale ?? 'es';
const direction = currentLocale === 'he' ? 'rtl' : 'ltr'; const direction = currentLocale === 'he' ? 'rtl' : 'ltr';
const newsSegments = Object.values(routeTranslations.news);
const isNewsPage = newsSegments.some(segment => Astro.url.pathname.includes(`/${segment}/`));
--- ---
<html lang={currentLocale} dir={direction} class="scroll-smooth"> <html lang={currentLocale} dir={direction} class="scroll-smooth">
@ -35,7 +39,7 @@ const direction = currentLocale === 'he' ? 'rtl' : 'ltr';
}); });
</script> </script>
<body class="font-primary"> <body class="font-primary">
{Astro.url.pathname.includes('/news/') && ( {isNewsPage && (
<ShareSticky client:only url={Astro.url.href} /> <ShareSticky client:only url={Astro.url.href} />
)} )}
<slot /> <slot />

View File

@ -0,0 +1,103 @@
---
import { YouTube } from "astro-embed";
import MainLayout from "../../../layouts/MainLayout.astro";
import Header from "../../../components/Header.astro";
import CarouselSection from "../../../components/section/CarouselSection.astro";
import { Image } from "@unpic/astro";
import { getCollection, render } from "astro:content";
import TitleSection from "../../../components/section/TitleSection.astro";
import FooterSection from "../../../components/section/FooterSection.astro";
import { getLocalizedRoute } from "@/i18n";
export const prerender = true;
// 1. Generate a new path for every collection entry
export async function getStaticPaths() {
const posts = await getCollection("news");
return posts.map((post) => ({
params: {
id: post.id,
locale: post.data.locale,
news_slug: getLocalizedRoute("news", post.data.locale),
},
props: { post },
}));
}
const { locale, news_slug } = Astro.params;
// 2. For your template, you can get the entry directly from the prop
const { post } = Astro.props;
const { Content } = await render(post);
const baseSlug = news_slug;
const rawContent = post.body || "";
const plainText = rawContent
.replace(/^#.*$/gm, "")
.replace(/^###.*$/gm, "")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/\*([^*]+)\*/g, "$1")
.replace(/_([^_]+)_/g, "$1")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/^>.*$/gm, "")
.replace(/`[^`]+`/g, "")
.replace(/^[-*]\s+/gm, "")
.trim();
const words = plainText
.split(/\s+/)
.filter((w) => w.length > 0)
.slice(0, 35);
const excerpt = words.join(" ") + (words.length === 35 ? "..." : "");
---
<MainLayout
title={post.data.title}
description={excerpt}
image={post.data.thumbnail}
url={new URL(`/${locale}/${news_slug}/${post.id}`, Astro.site)}
>
<div class="container mx-auto md:py-16 py-8">
<Header />
</div>
<a href={`/${locale}/${news_slug}/${post.id}`} class="block">
<TitleSection title={post.data.title} />
</a>
<div class="container mx-auto">
<a href={`/${locale}/${news_slug}/${post.id}`} class="block">
{
post.data.gallery?.length ? (
<CarouselSection images={post.data.gallery} />
) : post.data.thumbnail ? (
<Image
src={post.data.thumbnail}
alt={post.data.title}
class="hover:opacity-90 transition-opacity"
/>
) : null
}
</a>
</div>
<div class="grid md:grid-cols-10">
<div
class="md:col-span-7 content bg-white p-8 md:p-20 prose-p:mb-4 text-[#003421] text-justify"
>
<!-- <p class="text-lg font-semibold text-tertiary mb-8 pb-6 border-b border-tertiary/20 italic">
{excerpt}
</p> -->
<Content />
</div>
<div class="md:col-span-3 bg-tertiary md:sticky top-0 h-fit">
{post.data.youtube && <YouTube id={post.data.youtube} />}
{
post.data.gallery &&
post.data.gallery.map((galleryImage) => (
<Image src={galleryImage.image} alt={galleryImage.text} />
))
}
</div>
</div>
</MainLayout>
<FooterSection />

View File

@ -6,9 +6,21 @@ import { getCollection, getEntry } from "astro:content";
import FooterSection from "@/components/section/FooterSection.astro"; import FooterSection from "@/components/section/FooterSection.astro";
import { createTranslator } from '@/i18n'; import { createTranslator, getLocalizedRoute, routeTranslations } from '@/i18n';
const tl = createTranslator(Astro.currentLocale); const tl = createTranslator(Astro.currentLocale);
export function getStaticPaths() {
const locales = Object.keys(routeTranslations.news);
return locales.map((locale) => ({
params: {
locale,
news_slug: getLocalizedRoute('news', locale)
},
}));
}
const { locale, news_slug } = Astro.params;
const newsItems = await getCollection("news", (post)=>{ const newsItems = await getCollection("news", (post)=>{
const currentLocale = Astro.currentLocale; const currentLocale = Astro.currentLocale;
return post.data.locale == currentLocale return post.data.locale == currentLocale

View File

@ -1,95 +0,0 @@
---
import { YouTube } from 'astro-embed';
import MainLayout from "../../../layouts/MainLayout.astro";
import Header from "../../../components/Header.astro";
import CarouselSection from "../../../components/section/CarouselSection.astro";
import { Image } from "@unpic/astro";
import { getCollection, render } from "astro:content";
import TitleSection from "../../../components/section/TitleSection.astro";
import FooterSection from '../../../components/section/FooterSection.astro';
export const prerender = true;
// 1. Generate a new path for every collection entry
export async function getStaticPaths() {
const posts = await getCollection("news");
return posts.map((post) => ({
params: { id: post.id, locale: post.data.locale },
props: { post },
}));
}
const { locale } = Astro.params;
// 2. For your template, you can get the entry directly from the prop
const { post } = Astro.props;
const { Content } = await render(post);
const routeTranslations = {
news: {
es: "noticias",
en: "news",
fr: "actualites",
pt: "noticias",
de: "nachrichten",
}
};
const baseSlug = routeTranslations.news[locale] || routeTranslations.news.en;
const rawContent = post.body || "";
const plainText = rawContent
.replace(/^#.*$/gm, '')
.replace(/^###.*$/gm, '')
.replace(/\*\*([^*]+)\*\*/g, '$1')
.replace(/\*([^*]+)\*/g, '$1')
.replace(/_([^_]+)_/g, '$1')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/^>.*$/gm, '')
.replace(/`[^`]+`/g, '')
.replace(/^[-*]\s+/gm, '')
.trim();
const words = plainText.split(/\s+/).filter(w => w.length > 0).slice(0, 35);
const excerpt = words.join(' ') + (words.length === 35 ? '...' : '');
---
<MainLayout
title={post.data.title}
description={excerpt}
image={post.data.thumbnail}
url={new URL(`/${locale}/${"news"}/${post.id}`, Astro.site)}
>
<div class="container mx-auto md:py-16 py-8">
<Header />
</div>
<a href={`/${locale}/news/${post.id}`} class="block">
<TitleSection title={post.data.title} />
</a>
<div class="container mx-auto">
<a href={`/${locale}/news/${post.id}`} class="block">
{post.data.gallery?.length ? ( <CarouselSection images={post.data.gallery} />) : post.data.thumbnail ? (<Image src={post.data.thumbnail} alt={post.data.title} class="hover:opacity-90 transition-opacity" /> ) : null}
</a>
<div class="grid md:grid-cols-10">
<div class="md:col-span-7 content bg-white p-8 md:p-20 prose-p:mb-4 text-[#003421] text-justify">
<!-- <p class="text-lg font-semibold text-tertiary mb-8 pb-6 border-b border-tertiary/20 italic">
{excerpt}
</p> -->
<Content />
</div>
<div class="md:col-span-3 bg-tertiary md:sticky top-0 h-fit">
{ post.data.youtube && (
<YouTube id={post.data.youtube} />
)}
{post.data.gallery && (
post.data.gallery.map(galleryImage => (
<Image src={galleryImage.image} alt={galleryImage.text} />
))
)}
</div>
</div>
</MainLayout>
<FooterSection />

View File

@ -7,22 +7,27 @@ import { Image } from "@unpic/astro";
import { getCollection, render } from "astro:content"; import { getCollection, render } from "astro:content";
import TitleSection from "../../components/section/TitleSection.astro"; import TitleSection from "../../components/section/TitleSection.astro";
import FooterSection from '../../components/section/FooterSection.astro'; import FooterSection from '../../components/section/FooterSection.astro';
import { getLocalizedRoute } from '@/i18n';
export const prerender = true; export const prerender = true;
// 1. Generate a new path for every collection entry // 1. Generate a new path for every collection entry
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await getCollection("news"); const posts = await getCollection("news");
return posts.map((post) => ({ return posts.map((post) => ({
params: { id: post.id }, params: {
id: post.id,
news_slug: getLocalizedRoute('news', post.data.locale)
},
props: { post }, props: { post },
})); }));
} }
const { news_slug } = Astro.params;
const { post } = Astro.props; const { post } = Astro.props;
const { Content } = await render(post); const { Content } = await render(post);
console.log("astro site", Astro.site); console.log("astro site", Astro.site);
const baseUrl = Astro.site ?? "https://mk8nrc8p-4321.brs.devtunnels.ms"; const baseUrl = Astro.site ?? "https://mk8nrc8p-4321.brs.devtunnels.ms";
const pageUrl = new URL(`/es/news/${post.id}`, baseUrl).toString(); const pageUrl = new URL(`/${post.data.locale}/${news_slug}/${post.id}`, baseUrl).toString();
--- ---

View File

@ -8,9 +8,18 @@ import FooterSection from "@/components/section/FooterSection.astro";
import NewsList from "@/components/cards/NewsList.astro"; import NewsList from "@/components/cards/NewsList.astro";
import { createTranslator, t } from '@/i18n'; import { createTranslator, getLocalizedRoute, routeTranslations } from '@/i18n';
const tl = createTranslator(Astro.currentLocale); const tl = createTranslator(Astro.currentLocale);
export function getStaticPaths() {
const locales = Object.keys(routeTranslations.news);
return locales.map((locale) => ({
params: {
news_slug: getLocalizedRoute('news', locale)
},
}));
}
const newsItems = await getCollection("news", (post)=>{ const newsItems = await getCollection("news", (post)=>{
const currentLocale = Astro.currentLocale; const currentLocale = Astro.currentLocale;
return post.data.locale == currentLocale return post.data.locale == currentLocale