The landing was one address. It now also answers on the pages the panel publishes -- "Sobre nosotros" and the like -- at the addresses the API itself chooses: `path` and `alternates` put the default language unprefixed and every other one under its code, so `/sobre-nosotros`, `/en/about-us`, `/ca/qui-som` are the routes. `core/api.ts#contentRoute` is the single rule for that shape, and `/es/…` is deliberately not one of them, so a page has one address rather than two. Two routes share one load function, because Analog wants a `.server.ts` beside each `.page.ts`: `[slug]` for the default language, and `[locale]/[slug]` for the rest. Whether a slug exists is the API's answer, not something the route table knows, so the 404 middleware lets page-shaped addresses through and `ui/content-page.ts` sets the status itself from `ngOnInit` -- the same `injectResponse()` the landing uses for its 503, and `ngOnInit` because an input has no value in the constructor. Body HTML comes from the panel's editor and goes through Angular's sanitiser. The header shows the featured pages beside the section anchors; the footer lists them all. On a page the language switcher follows the API's `alternates`, and a language with no translation leads to the landing in that language instead of a 404. Verified: lint, 20 unit tests, build, and 6 e2e -- including a new one that a page-shaped address the API does not know answers 404 with `robots: noindex`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
405 lines
12 KiB
TypeScript
405 lines
12 KiB
TypeScript
/**
|
||
* The chrome's own words. Content arrives from the API already resolved to
|
||
* one language; headings, labels and button text are ours, so they live here
|
||
* and are picked with the same locale.
|
||
*/
|
||
import type { Locale } from './api';
|
||
|
||
export interface Copy {
|
||
nav: {
|
||
services: string;
|
||
projects: string;
|
||
team: string;
|
||
contribute: string;
|
||
contact: string;
|
||
darkMode: string;
|
||
lightMode: string;
|
||
};
|
||
hero: {
|
||
eyebrow: string;
|
||
title: string;
|
||
highlight: string;
|
||
lead: string;
|
||
primaryCta: string;
|
||
secondaryCta: string;
|
||
};
|
||
stats: Record<
|
||
| 'projects'
|
||
| 'active_projects'
|
||
| 'members'
|
||
| 'open_calls'
|
||
| 'services'
|
||
| 'clients'
|
||
| 'good_first_issues'
|
||
| 'stars',
|
||
string
|
||
>;
|
||
sections: {
|
||
services: { title: string; lead: string };
|
||
projects: { title: string; lead: string };
|
||
clients: { title: string; lead: string };
|
||
testimonials: { title: string; lead: string };
|
||
radar: { title: string; lead: string };
|
||
team: { title: string; lead: string };
|
||
calls: { title: string; lead: string };
|
||
issues: { title: string; lead: string };
|
||
talks: { title: string; lead: string };
|
||
contact: { title: string; lead: string };
|
||
};
|
||
projectStatus: Record<
|
||
'idea' | 'active' | 'paused' | 'completed' | 'archived',
|
||
string
|
||
>;
|
||
rings: Record<'adopt' | 'trial' | 'assess' | 'hold', string>;
|
||
labels: {
|
||
demo: string;
|
||
repository: string;
|
||
stars: string;
|
||
needsHelp: string;
|
||
founder: string;
|
||
alumni: string;
|
||
hoursPerWeek: string;
|
||
weeks: string;
|
||
seniority: string;
|
||
closes: string;
|
||
apply: string;
|
||
watchTalk: string;
|
||
slides: string;
|
||
openIssue: string;
|
||
comments: string;
|
||
empty: string;
|
||
seeMore: string;
|
||
};
|
||
form: {
|
||
kind: string;
|
||
kindContribute: string;
|
||
kindRequest: string;
|
||
name: string;
|
||
email: string;
|
||
organisation: string;
|
||
hours: string;
|
||
hoursHint: string;
|
||
message: string;
|
||
messageHint: string;
|
||
consent: string;
|
||
submit: string;
|
||
sending: string;
|
||
success: string;
|
||
duplicate: string;
|
||
error: string;
|
||
closed: string;
|
||
};
|
||
footer: { rights: string; pages: string };
|
||
page: { back: string; skip: string };
|
||
maintenance: { title: string; back: string; until: string };
|
||
}
|
||
|
||
const es: Copy = {
|
||
nav: {
|
||
services: 'Servicios',
|
||
projects: 'Proyectos',
|
||
team: 'Equipo',
|
||
contribute: 'Colabora',
|
||
contact: 'Contacto',
|
||
darkMode: 'Modo oscuro',
|
||
lightMode: 'Modo claro',
|
||
},
|
||
hero: {
|
||
eyebrow: 'Colectivo de desarrollo',
|
||
title: 'Construimos software',
|
||
highlight: 'en abierto',
|
||
lead: 'Proyectos reales, código público y gente que enseña lo que aprende. Si tienes una idea que empujar o unas horas que aportar, este es el sitio.',
|
||
primaryCta: 'Ver proyectos',
|
||
secondaryCta: 'Hablemos',
|
||
},
|
||
stats: {
|
||
projects: 'Proyectos',
|
||
active_projects: 'En marcha',
|
||
members: 'Personas',
|
||
open_calls: 'Vacantes abiertas',
|
||
services: 'Servicios',
|
||
clients: 'Clientes',
|
||
good_first_issues: 'Issues para empezar',
|
||
stars: 'Estrellas',
|
||
},
|
||
sections: {
|
||
services: { title: 'Qué hacemos', lead: 'Lo que alguien nos pediría.' },
|
||
projects: { title: 'Proyectos', lead: 'Lo que ya hemos construido.' },
|
||
clients: { title: 'Con quién trabajamos', lead: 'Organizaciones detrás del trabajo.' },
|
||
testimonials: { title: 'Lo que dicen', lead: 'Palabras de quien nos contrató.' },
|
||
radar: { title: 'Radar técnico', lead: 'Qué adoptamos, qué probamos y qué dejamos pasar.' },
|
||
team: { title: 'Quiénes somos', lead: 'Las personas detrás de todo esto.' },
|
||
calls: { title: 'Buscamos gente', lead: 'Vacantes abiertas ahora mismo.' },
|
||
issues: { title: 'Primeras contribuciones', lead: 'Issues pensadas para empezar hoy.' },
|
||
talks: { title: 'Charlas', lead: 'Lo que hemos contado fuera.' },
|
||
contact: { title: 'Escríbenos', lead: 'Cuéntanos qué necesitas o qué te apetece hacer.' },
|
||
},
|
||
projectStatus: {
|
||
idea: 'Idea',
|
||
active: 'Activo',
|
||
paused: 'En pausa',
|
||
completed: 'Terminado',
|
||
archived: 'Archivado',
|
||
},
|
||
rings: { adopt: 'Adoptar', trial: 'Probar', assess: 'Evaluar', hold: 'Esperar' },
|
||
labels: {
|
||
demo: 'Demo',
|
||
repository: 'Repositorio',
|
||
stars: 'estrellas',
|
||
needsHelp: 'Busca ayuda',
|
||
founder: 'Fundador/a',
|
||
alumni: 'Alumni',
|
||
hoursPerWeek: 'h/semana',
|
||
weeks: 'semanas',
|
||
seniority: 'Nivel',
|
||
closes: 'Cierra el',
|
||
apply: 'Me interesa',
|
||
watchTalk: 'Ver charla',
|
||
slides: 'Diapositivas',
|
||
openIssue: 'Abrir issue',
|
||
comments: 'comentarios',
|
||
empty: 'Todavía no hay nada publicado aquí.',
|
||
seeMore: 'Ver más',
|
||
},
|
||
form: {
|
||
kind: 'Escribes para…',
|
||
kindContribute: 'Aportar tiempo',
|
||
kindRequest: 'Pedir ayuda para un proyecto',
|
||
name: 'Nombre',
|
||
email: 'Correo',
|
||
organisation: 'Organización',
|
||
hours: 'Horas por semana',
|
||
hoursHint: 'Por ejemplo, 4',
|
||
message: 'Mensaje',
|
||
messageHint: 'Mínimo 20 caracteres.',
|
||
consent: 'Acepto que guardéis mis datos para responderme.',
|
||
submit: 'Enviar',
|
||
sending: 'Enviando…',
|
||
success: 'Mensaje recibido. Te contestamos en cuanto podamos.',
|
||
duplicate: 'Ya teníamos este mensaje. No hace falta reenviarlo.',
|
||
error: 'No hemos podido enviarlo. Inténtalo en un rato.',
|
||
closed: 'El formulario está cerrado temporalmente.',
|
||
},
|
||
footer: {
|
||
rights: 'Todos los derechos reservados.',
|
||
pages: 'Más sobre Pireon',
|
||
},
|
||
page: {
|
||
back: 'Volver al inicio',
|
||
skip: 'Saltar al contenido',
|
||
},
|
||
maintenance: {
|
||
title: 'Estamos de mantenimiento',
|
||
back: 'Volvemos en un rato.',
|
||
until: 'Volvemos el',
|
||
},
|
||
};
|
||
|
||
const en: Copy = {
|
||
nav: {
|
||
services: 'Services',
|
||
projects: 'Projects',
|
||
team: 'Team',
|
||
contribute: 'Contribute',
|
||
contact: 'Contact',
|
||
darkMode: 'Dark mode',
|
||
lightMode: 'Light mode',
|
||
},
|
||
hero: {
|
||
eyebrow: 'A development collective',
|
||
title: 'We build software',
|
||
highlight: 'in the open',
|
||
lead: 'Real projects, public code and people who teach what they learn. If you have an idea to push or a few hours to give, you are in the right place.',
|
||
primaryCta: 'See projects',
|
||
secondaryCta: "Let's talk",
|
||
},
|
||
stats: {
|
||
projects: 'Projects',
|
||
active_projects: 'In progress',
|
||
members: 'People',
|
||
open_calls: 'Open calls',
|
||
services: 'Services',
|
||
clients: 'Clients',
|
||
good_first_issues: 'Good first issues',
|
||
stars: 'Stars',
|
||
},
|
||
sections: {
|
||
services: { title: 'What we do', lead: 'What somebody would be asking for.' },
|
||
projects: { title: 'Projects', lead: 'What we have already built.' },
|
||
clients: { title: 'Who we work with', lead: 'The organisations behind the work.' },
|
||
testimonials: { title: 'What they said', lead: 'Words from the people who hired us.' },
|
||
radar: { title: 'Tech radar', lead: 'What we adopt, trial, assess and hold.' },
|
||
team: { title: 'Who we are', lead: 'The people behind all of this.' },
|
||
calls: { title: 'We are looking for people', lead: 'Calls open right now.' },
|
||
issues: { title: 'First contributions', lead: 'Issues meant to be picked up today.' },
|
||
talks: { title: 'Talks', lead: 'What we have told elsewhere.' },
|
||
contact: { title: 'Write to us', lead: 'Tell us what you need, or what you feel like building.' },
|
||
},
|
||
projectStatus: {
|
||
idea: 'Idea',
|
||
active: 'Active',
|
||
paused: 'Paused',
|
||
completed: 'Completed',
|
||
archived: 'Archived',
|
||
},
|
||
rings: { adopt: 'Adopt', trial: 'Trial', assess: 'Assess', hold: 'Hold' },
|
||
labels: {
|
||
demo: 'Demo',
|
||
repository: 'Repository',
|
||
stars: 'stars',
|
||
needsHelp: 'Needs help',
|
||
founder: 'Founder',
|
||
alumni: 'Alumni',
|
||
hoursPerWeek: 'h/week',
|
||
weeks: 'weeks',
|
||
seniority: 'Level',
|
||
closes: 'Closes on',
|
||
apply: "I'm interested",
|
||
watchTalk: 'Watch talk',
|
||
slides: 'Slides',
|
||
openIssue: 'Open issue',
|
||
comments: 'comments',
|
||
empty: 'Nothing published here yet.',
|
||
seeMore: 'See more',
|
||
},
|
||
form: {
|
||
kind: 'You are writing to…',
|
||
kindContribute: 'Give some time',
|
||
kindRequest: 'Ask for help with a project',
|
||
name: 'Name',
|
||
email: 'E-mail',
|
||
organisation: 'Organisation',
|
||
hours: 'Hours per week',
|
||
hoursHint: 'For example, 4',
|
||
message: 'Message',
|
||
messageHint: 'At least 20 characters.',
|
||
consent: 'I agree to have my details stored so you can reply.',
|
||
submit: 'Send',
|
||
sending: 'Sending…',
|
||
success: 'Message received. We will get back to you as soon as we can.',
|
||
duplicate: 'We already had this message. No need to send it again.',
|
||
error: 'We could not send it. Please try again in a while.',
|
||
closed: 'The form is temporarily closed.',
|
||
},
|
||
footer: {
|
||
rights: 'All rights reserved.',
|
||
pages: 'More about Pireon',
|
||
},
|
||
page: {
|
||
back: 'Back to home',
|
||
skip: 'Skip to content',
|
||
},
|
||
maintenance: {
|
||
title: 'We are under maintenance',
|
||
back: 'Back in a while.',
|
||
until: 'Back on',
|
||
},
|
||
};
|
||
|
||
const ca: Copy = {
|
||
nav: {
|
||
services: 'Serveis',
|
||
projects: 'Projectes',
|
||
team: 'Equip',
|
||
contribute: 'Col·labora',
|
||
contact: 'Contacte',
|
||
darkMode: 'Mode fosc',
|
||
lightMode: 'Mode clar',
|
||
},
|
||
hero: {
|
||
eyebrow: 'Col·lectiu de desenvolupament',
|
||
title: 'Construïm programari',
|
||
highlight: 'en obert',
|
||
lead: 'Projectes reals, codi públic i gent que ensenya el que aprèn. Si tens una idea per empènyer o unes hores per aportar, aquest és el lloc.',
|
||
primaryCta: 'Veure projectes',
|
||
secondaryCta: 'Parlem-ne',
|
||
},
|
||
stats: {
|
||
projects: 'Projectes',
|
||
active_projects: 'En marxa',
|
||
members: 'Persones',
|
||
open_calls: 'Vacants obertes',
|
||
services: 'Serveis',
|
||
clients: 'Clients',
|
||
good_first_issues: 'Issues per començar',
|
||
stars: 'Estrelles',
|
||
},
|
||
sections: {
|
||
services: { title: 'Què fem', lead: 'El que algú ens demanaria.' },
|
||
projects: { title: 'Projectes', lead: 'El que ja hem construït.' },
|
||
clients: { title: 'Amb qui treballem', lead: 'Organitzacions darrere de la feina.' },
|
||
testimonials: { title: 'Què en diuen', lead: 'Paraules de qui ens ha contractat.' },
|
||
radar: { title: 'Radar tècnic', lead: 'Què adoptem, què provem i què deixem passar.' },
|
||
team: { title: 'Qui som', lead: 'Les persones darrere de tot això.' },
|
||
calls: { title: 'Busquem gent', lead: 'Vacants obertes ara mateix.' },
|
||
issues: { title: 'Primeres contribucions', lead: 'Issues pensades per començar avui.' },
|
||
talks: { title: 'Xerrades', lead: 'El que hem explicat a fora.' },
|
||
contact: { title: 'Escriu-nos', lead: 'Explica què necessites o què et ve de gust fer.' },
|
||
},
|
||
projectStatus: {
|
||
idea: 'Idea',
|
||
active: 'Actiu',
|
||
paused: 'En pausa',
|
||
completed: 'Acabat',
|
||
archived: 'Arxivat',
|
||
},
|
||
rings: { adopt: 'Adoptar', trial: 'Provar', assess: 'Avaluar', hold: 'Esperar' },
|
||
labels: {
|
||
demo: 'Demo',
|
||
repository: 'Repositori',
|
||
stars: 'estrelles',
|
||
needsHelp: 'Busca ajuda',
|
||
founder: 'Fundador/a',
|
||
alumni: 'Alumni',
|
||
hoursPerWeek: 'h/setmana',
|
||
weeks: 'setmanes',
|
||
seniority: 'Nivell',
|
||
closes: 'Tanca el',
|
||
apply: "M'interessa",
|
||
watchTalk: 'Veure xerrada',
|
||
slides: 'Diapositives',
|
||
openIssue: 'Obrir issue',
|
||
comments: 'comentaris',
|
||
empty: 'Encara no hi ha res publicat aquí.',
|
||
seeMore: 'Veure més',
|
||
},
|
||
form: {
|
||
kind: 'Escrius per…',
|
||
kindContribute: 'Aportar temps',
|
||
kindRequest: 'Demanar ajuda per a un projecte',
|
||
name: 'Nom',
|
||
email: 'Correu',
|
||
organisation: 'Organització',
|
||
hours: 'Hores per setmana',
|
||
hoursHint: 'Per exemple, 4',
|
||
message: 'Missatge',
|
||
messageHint: 'Mínim 20 caràcters.',
|
||
consent: 'Accepto que deseu les meves dades per respondre’m.',
|
||
submit: 'Enviar',
|
||
sending: 'Enviant…',
|
||
success: 'Missatge rebut. Et contestem tan aviat com puguem.',
|
||
duplicate: 'Ja teníem aquest missatge. No cal reenviar-lo.',
|
||
error: 'No hem pogut enviar-lo. Torna-ho a provar d’aquí una estona.',
|
||
closed: 'El formulari està tancat temporalment.',
|
||
},
|
||
footer: {
|
||
rights: 'Tots els drets reservats.',
|
||
pages: 'Més sobre Pireon',
|
||
},
|
||
page: {
|
||
back: "Tornar a l'inici",
|
||
skip: 'Salta al contingut',
|
||
},
|
||
maintenance: {
|
||
title: 'Estem de manteniment',
|
||
back: 'Tornem d’aquí una estona.',
|
||
until: 'Tornem el',
|
||
},
|
||
};
|
||
|
||
const dictionaries: Record<Locale, Copy> = { es, en, ca };
|
||
|
||
export function copyFor(locale: Locale): Copy {
|
||
return dictionaries[locale] ?? dictionaries.es;
|
||
}
|