From 0099da62f703c0461b2468aa3a68e0c5c78f378a Mon Sep 17 00:00:00 2001 From: Zack Reneau-Wedeen Date: Tue, 7 Nov 2023 11:45:51 -0800 Subject: [PATCH] Initial localization of status pages to es, fr, de --- apps/web/src/app/[locale]/layout.tsx | 13 ++++++ .../[domain]/_components/navigation-link.tsx | 0 .../[domain]/_components/switcher.tsx | 44 +++++++++++++++++++ .../[domain]/_components/user-button.tsx | 0 .../status-page/[domain]/incidents/page.tsx | 6 ++- .../status-page/[domain]/layout.tsx | 14 ++++-- .../status-page/[domain]/loading.tsx | 0 .../status-page/[domain]/page.tsx | 14 +++--- .../components/status-page/incident-list.tsx | 14 +++--- .../src/components/status-page/monitor.tsx | 4 +- apps/web/src/components/tracker.tsx | 8 ++-- apps/web/src/middleware.ts | 13 ++++++ apps/web/src/yuzu/client.ts | 10 +++++ apps/web/src/yuzu/de.json | 1 + apps/web/src/yuzu/en.json | 1 + apps/web/src/yuzu/es.json | 1 + apps/web/src/yuzu/fr.json | 1 + apps/web/src/yuzu/server.ts | 8 ++++ apps/web/yuzu.config.ts | 22 ++++++++++ 19 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 apps/web/src/app/[locale]/layout.tsx rename apps/web/src/app/{ => [locale]}/status-page/[domain]/_components/navigation-link.tsx (100%) create mode 100644 apps/web/src/app/[locale]/status-page/[domain]/_components/switcher.tsx rename apps/web/src/app/{ => [locale]}/status-page/[domain]/_components/user-button.tsx (100%) rename apps/web/src/app/{ => [locale]}/status-page/[domain]/incidents/page.tsx (88%) rename apps/web/src/app/{ => [locale]}/status-page/[domain]/layout.tsx (74%) rename apps/web/src/app/{ => [locale]}/status-page/[domain]/loading.tsx (100%) rename apps/web/src/app/{ => [locale]}/status-page/[domain]/page.tsx (86%) create mode 100644 apps/web/src/yuzu/client.ts create mode 100644 apps/web/src/yuzu/de.json create mode 100644 apps/web/src/yuzu/en.json create mode 100644 apps/web/src/yuzu/es.json create mode 100644 apps/web/src/yuzu/fr.json create mode 100644 apps/web/src/yuzu/server.ts create mode 100644 apps/web/yuzu.config.ts diff --git a/apps/web/src/app/[locale]/layout.tsx b/apps/web/src/app/[locale]/layout.tsx new file mode 100644 index 0000000000..2665728e75 --- /dev/null +++ b/apps/web/src/app/[locale]/layout.tsx @@ -0,0 +1,13 @@ +import { I18nProviderClient } from '@/yuzu/client'; + +export default async function StatusPageI18nLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/apps/web/src/app/status-page/[domain]/_components/navigation-link.tsx b/apps/web/src/app/[locale]/status-page/[domain]/_components/navigation-link.tsx similarity index 100% rename from apps/web/src/app/status-page/[domain]/_components/navigation-link.tsx rename to apps/web/src/app/[locale]/status-page/[domain]/_components/navigation-link.tsx diff --git a/apps/web/src/app/[locale]/status-page/[domain]/_components/switcher.tsx b/apps/web/src/app/[locale]/status-page/[domain]/_components/switcher.tsx new file mode 100644 index 0000000000..8e6e492b49 --- /dev/null +++ b/apps/web/src/app/[locale]/status-page/[domain]/_components/switcher.tsx @@ -0,0 +1,44 @@ +'use client' + +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem +} from '@openstatus/ui' +import { useChangeLocale, useCurrentLocale } from '@/yuzu/client' +import yuzuConfig from '@/../yuzu.config' +const locales = yuzuConfig.locales + +export function Switcher() { + const changeLocale = useChangeLocale() + const locale = useCurrentLocale() + + function onSwitch(value: typeof locales[number]['code']) { + changeLocale(value) + } + + return ( +
+ +
+ ) +} diff --git a/apps/web/src/app/status-page/[domain]/_components/user-button.tsx b/apps/web/src/app/[locale]/status-page/[domain]/_components/user-button.tsx similarity index 100% rename from apps/web/src/app/status-page/[domain]/_components/user-button.tsx rename to apps/web/src/app/[locale]/status-page/[domain]/_components/user-button.tsx diff --git a/apps/web/src/app/status-page/[domain]/incidents/page.tsx b/apps/web/src/app/[locale]/status-page/[domain]/incidents/page.tsx similarity index 88% rename from apps/web/src/app/status-page/[domain]/incidents/page.tsx rename to apps/web/src/app/[locale]/status-page/[domain]/incidents/page.tsx index 9620955816..a055e1ed4a 100644 --- a/apps/web/src/app/status-page/[domain]/incidents/page.tsx +++ b/apps/web/src/app/[locale]/status-page/[domain]/incidents/page.tsx @@ -9,6 +9,7 @@ import { import { Header } from "@/components/dashboard/header"; import { IncidentList } from "@/components/status-page/incident-list"; import { api } from "@/trpc/server"; +import { getI18n } from '@/yuzu/server'; type Props = { params: { domain: string }; @@ -36,6 +37,7 @@ export default async function Page({ params }: Props) { export async function generateMetadata({ params }: Props): Promise { const page = await api.page.getPageBySlug.query({ slug: params.domain }); const firstMonitor = page?.monitors?.[0]; // temporary solution + const t = await getI18n(); return { ...defaultMetadata, @@ -46,7 +48,7 @@ export async function generateMetadata({ params }: Props): Promise { ...twitterMetadata, images: [ `/api/og?monitorId=${firstMonitor?.id}&title=${page?.title}&description=${ - page?.description || `The ${page?.title} status page` + page?.description || `${t('The')} ${page?.title} ${t('status page')}` }`, ], title: page?.title, @@ -56,7 +58,7 @@ export async function generateMetadata({ params }: Props): Promise { ...ogMetadata, images: [ `/api/og?monitorId=${firstMonitor?.id}&title=${page?.title}&description=${ - page?.description || `The ${page?.title} status page` + page?.description || `${t('The')} ${page?.title} ${t('status page')}` }`, ], title: page?.title, diff --git a/apps/web/src/app/status-page/[domain]/layout.tsx b/apps/web/src/app/[locale]/status-page/[domain]/layout.tsx similarity index 74% rename from apps/web/src/app/status-page/[domain]/layout.tsx rename to apps/web/src/app/[locale]/status-page/[domain]/layout.tsx index 496c0cc640..e344b1a436 100644 --- a/apps/web/src/app/status-page/[domain]/layout.tsx +++ b/apps/web/src/app/[locale]/status-page/[domain]/layout.tsx @@ -1,18 +1,23 @@ import { Shell } from "@/components/dashboard/shell"; import NavigationLink from "./_components/navigation-link"; import { UserButton } from "./_components/user-button"; +import { Switcher } from './_components/switcher'; +import { getI18n } from '@/yuzu/server'; -export default function StatusPageLayout({ +export default async function StatusPageLayout({ children, }: { children: React.ReactNode; }) { + + const t = await getI18n(); + return (
- Status - Incidents + {t('Status')} + {t('Incidents')}
@@ -22,7 +27,7 @@ export default function StatusPageLayout({

- powered by{" "} + {t('powered by')}{" "}

+
); diff --git a/apps/web/src/app/status-page/[domain]/loading.tsx b/apps/web/src/app/[locale]/status-page/[domain]/loading.tsx similarity index 100% rename from apps/web/src/app/status-page/[domain]/loading.tsx rename to apps/web/src/app/[locale]/status-page/[domain]/loading.tsx diff --git a/apps/web/src/app/status-page/[domain]/page.tsx b/apps/web/src/app/[locale]/status-page/[domain]/page.tsx similarity index 86% rename from apps/web/src/app/status-page/[domain]/page.tsx rename to apps/web/src/app/[locale]/status-page/[domain]/page.tsx index c636bfb4ad..f2dee87c62 100644 --- a/apps/web/src/app/status-page/[domain]/page.tsx +++ b/apps/web/src/app/[locale]/status-page/[domain]/page.tsx @@ -15,6 +15,7 @@ import { IncidentList } from "@/components/status-page/incident-list"; import { MonitorList } from "@/components/status-page/monitor-list"; import { StatusCheck } from "@/components/status-page/status-check"; import { api } from "@/trpc/server"; +import { getI18n } from '@/yuzu/server'; const url = process.env.NODE_ENV === "development" @@ -39,6 +40,8 @@ export default async function Page({ params }: Props) { Boolean(page.monitors.length) || Boolean(page.incidents.length) ); + const t = await getI18n(); + return (
- Go to Dashboard + {t('Go to Dashboard')} } /> @@ -75,6 +78,7 @@ export default async function Page({ params }: Props) { export async function generateMetadata({ params }: Props): Promise { const page = await api.page.getPageBySlug.query({ slug: params.domain }); const firstMonitor = page?.monitors?.[0]; // temporary solution + const t = await getI18n(); return { ...defaultMetadata, @@ -85,7 +89,7 @@ export async function generateMetadata({ params }: Props): Promise { ...twitterMetadata, images: [ `/api/og?monitorId=${firstMonitor?.id}&title=${page?.title}&description=${ - page?.description || `The ${page?.title} status page` + page?.description || `${t('The')} ${page?.title} ${t('status page')}` }`, ], title: page?.title, @@ -95,7 +99,7 @@ export async function generateMetadata({ params }: Props): Promise { ...ogMetadata, images: [ `/api/og?monitorId=${firstMonitor?.id}&title=${page?.title}&description=${ - page?.description || `The ${page?.title} status page` + page?.description || `${t('The')} ${page?.title} ${t('status page')}` }`, ], title: page?.title, diff --git a/apps/web/src/components/status-page/incident-list.tsx b/apps/web/src/components/status-page/incident-list.tsx index 0dd0db4247..1c83f82e02 100644 --- a/apps/web/src/components/status-page/incident-list.tsx +++ b/apps/web/src/components/status-page/incident-list.tsx @@ -9,10 +9,11 @@ import { notEmpty } from "@/lib/utils"; import { AffectedMonitors } from "../incidents/affected-monitors"; import { Events } from "../incidents/events"; import { StatusBadge } from "../incidents/status-badge"; +import { getI18n } from '@/yuzu/server'; // TODO: change layout - it is too packed with data rn -export const IncidentList = ({ +export const IncidentList = async ({ incidents, monitors, context = "all", @@ -21,6 +22,7 @@ export const IncidentList = ({ monitors: z.infer[]; context?: "all" | "latest"; // latest 7 days }) => { + const t = await getI18n(); const lastWeek = Date.now() - 1000 * 60 * 60 * 24 * 7; function getLastWeeksIncidents() { @@ -42,7 +44,7 @@ export const IncidentList = ({ })?.length > 0 ? (

- {context === "all" ? "All incidents" : "Latest incidents"} + {context === "all" ? t("All incidents") : t("Latest incidents")}

{_incidents.map((incident) => { const affectedMonitors = incident.monitorsToIncidents @@ -60,7 +62,7 @@ export const IncidentList = ({ {Boolean(affectedMonitors.length) ? (

- Affected Monitors + {t('Affected Monitors')}

- Latest Updates + {t('Latest Updates')}

@@ -87,8 +89,8 @@ export const IncidentList = ({ ) : (

{context === "all" - ? "No incidents." - : "No incidents in the last week."} + ? t("No incidents.") + : t("No incidents in the last week.")}

)} diff --git a/apps/web/src/components/status-page/monitor.tsx b/apps/web/src/components/status-page/monitor.tsx index 21676e7b21..347435d6b7 100644 --- a/apps/web/src/components/status-page/monitor.tsx +++ b/apps/web/src/components/status-page/monitor.tsx @@ -4,6 +4,7 @@ import type { selectPublicMonitorSchema } from "@openstatus/db/src/schema"; import { getMonitorListData } from "@/lib/tb"; import { Tracker } from "../tracker"; +import { getI18n } from '@/yuzu/server'; export const Monitor = async ({ monitor, @@ -11,7 +12,8 @@ export const Monitor = async ({ monitor: z.infer; }) => { const data = await getMonitorListData({ monitorId: String(monitor.id) }); - if (!data) return
Something went wrong
; + const t = await getI18n(); + if (!data) return
{t('Something went wrong')}
; return ( ) => { const [open, setOpen] = React.useState(false); + const t = useI18n() const ratio = ok / count; const date = new Date(cronTimestamp); const toDate = date.setDate(date.getDate() + 1); @@ -169,7 +171,7 @@ const Bar = ({ {format(new Date(cronTimestamp), dateFormat)}

- avg. {avgLatency}ms + {t('avg.')} {avgLatency}{t('ms')}

@@ -177,13 +179,13 @@ const Bar = ({

{count}{" "} - total requests + {t('total requests')}

{count - ok}{" "} - failed requests + {t('failed requests')}

diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 4eaa59416f..451835fa2a 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -9,9 +9,16 @@ import { usersToWorkspaces, workspace, } from "@openstatus/db/src/schema"; +import { createI18nMiddleware } from 'next-international/middleware' import { env } from "./env"; +const I18nMiddleware = createI18nMiddleware({ + locales: ['en', 'es', 'fr', 'de'], + defaultLocale: 'en', + urlMappingStrategy: 'rewriteDefault' +}) + const before = (req: NextRequest) => { const url = req.nextUrl.clone(); @@ -85,6 +92,12 @@ export default authMiddleware({ beforeAuth: before, debug: false, async afterAuth(auth, req) { + const host = req.headers.get("host"); + const subdomain = getValidSubdomain(host); + if (subdomain || req.nextUrl.pathname.includes('/status-page/')) { + return I18nMiddleware(req) + } + // handle users who aren't authenticated if (!auth.userId && !auth.isPublicRoute) { return redirectToSignIn({ returnBackUrl: req.url }); diff --git a/apps/web/src/yuzu/client.ts b/apps/web/src/yuzu/client.ts new file mode 100644 index 0000000000..0509b0fd46 --- /dev/null +++ b/apps/web/src/yuzu/client.ts @@ -0,0 +1,10 @@ +'use client' + +import { createI18nClient } from 'next-international/client' + +export const { useCurrentLocale, useChangeLocale, useI18n, useScopedI18n, I18nProviderClient } = createI18nClient({ + 'en': () => import('./en.json'), + 'es': () => import('./es.json'), + 'fr': () => import('./fr.json'), + 'de': () => import('./de.json') +}) \ No newline at end of file diff --git a/apps/web/src/yuzu/de.json b/apps/web/src/yuzu/de.json new file mode 100644 index 0000000000..a7eab1faba --- /dev/null +++ b/apps/web/src/yuzu/de.json @@ -0,0 +1 @@ +{"avg.":"Durchschnittswert","ms":"ms","total requests":"Anfragen insgesamt","failed requests":"fehlgeschlagene Anfragen","Something went wrong":"Etwas ist schief gelaufen","All incidents":"Alle Vorfälle","Latest incidents":"Letzte Vorfälle","Affected Monitors":"Betroffene Monitore","Latest Updates":"Letzte Updates","No incidents.":"Keine Vorfälle.","No incidents in the last week.":"Keine Vorfälle in der letzten Woche.","Missing Monitors":"Fehlende Monitore","Fill your status page with monitors.":"Füllen Sie Ihre Statusseite mit Monitoren.","Go to Dashboard":"Zum Dashboard gehen","The":"Die","status page":"Statusseite","Status":"Status","Incidents":"Vorfälle","powered by":"angetrieben von"} \ No newline at end of file diff --git a/apps/web/src/yuzu/en.json b/apps/web/src/yuzu/en.json new file mode 100644 index 0000000000..268c7a6e29 --- /dev/null +++ b/apps/web/src/yuzu/en.json @@ -0,0 +1 @@ +{"avg.":"avg.","ms":"ms","total requests":"total requests","failed requests":"failed requests","Something went wrong":"Something went wrong","All incidents":"All incidents","Latest incidents":"Latest incidents","Affected Monitors":"Affected Monitors","Latest Updates":"Latest Updates","No incidents.":"No incidents.","No incidents in the last week.":"No incidents in the last week.","Missing Monitors":"Missing Monitors","Fill your status page with monitors.":"Fill your status page with monitors.","Go to Dashboard":"Go to Dashboard","The":"The","status page":"status page","Status":"Status","Incidents":"Incidents","powered by":"powered by"} \ No newline at end of file diff --git a/apps/web/src/yuzu/es.json b/apps/web/src/yuzu/es.json new file mode 100644 index 0000000000..ce5065d997 --- /dev/null +++ b/apps/web/src/yuzu/es.json @@ -0,0 +1 @@ +{"avg.":"promedio","ms":"ms","total requests":"total solicitudes","failed requests":"solicitudes fallidas","Something went wrong":"Algo salió mal","All incidents":"Todos los incidentes","Latest incidents":"Últimos incidentes","Affected Monitors":"Monitores afectados","Latest Updates":"Últimas actualizaciones","No incidents.":"Ningún incidente.","No incidents in the last week.":"Ningún incidente en la última semana.","Missing Monitors":"Monitores desaparecidos","Fill your status page with monitors.":"Llena tu página de estado con monitores.","Go to Dashboard":"Ir al panel de control","The":"En","status page":"página de estado","Status":"Estado","Incidents":"Incidentes","powered by":"accionado por"} \ No newline at end of file diff --git a/apps/web/src/yuzu/fr.json b/apps/web/src/yuzu/fr.json new file mode 100644 index 0000000000..637f07cfca --- /dev/null +++ b/apps/web/src/yuzu/fr.json @@ -0,0 +1 @@ +{"avg.":"moyenne.","ms":"ms","total requests":"total des demandes","failed requests":"Demandes rejetées","Something went wrong":"Quelque chose n'a pas fonctionné","All incidents":"Tous les incidents","Latest incidents":"Derniers incidents","Affected Monitors":"Moniteurs concernés","Latest Updates":"Dernières mises à jour","No incidents.":"Aucun incident.","No incidents in the last week.":"Aucun incident au cours de la semaine écoulée.","Missing Monitors":"Moniteurs manquants","Fill your status page with monitors.":"Remplissez votre page de statut avec des moniteurs.","Go to Dashboard":"Accéder au tableau de bord","The":"Les","status page":"page d'état","Status":"Statut","Incidents":"Incidents","powered by":"alimenté par"} \ No newline at end of file diff --git a/apps/web/src/yuzu/server.ts b/apps/web/src/yuzu/server.ts new file mode 100644 index 0000000000..174e304a67 --- /dev/null +++ b/apps/web/src/yuzu/server.ts @@ -0,0 +1,8 @@ +import { createI18nServer } from 'next-international/server' + +export const { getCurrentLocale, getI18n, getScopedI18n, getStaticParams } = createI18nServer({ + 'en': () => import('./en.json'), + 'es': () => import('./es.json'), + 'fr': () => import('./fr.json'), + 'de': () => import('./de.json') +}) \ No newline at end of file diff --git a/apps/web/yuzu.config.ts b/apps/web/yuzu.config.ts new file mode 100644 index 0000000000..b7cb5a46d2 --- /dev/null +++ b/apps/web/yuzu.config.ts @@ -0,0 +1,22 @@ +export default { + locales: [{ + code: 'en', + name: 'English', + }, { + code: 'es', + name: 'Español', + }, { + code: 'fr', + name: 'Français', + }, { + code: 'de', + name: 'Deutsch', + }], + resources: 'src/yuzu', + content: [ + 'src/**/*.{tsx,ts,jsx,js}' + ], + transformers: ['t', 'scopedT'], + framework: 'nextjs', + tsx: true, +} as const \ No newline at end of file