diff --git a/apps/web/src/app/[locale]/layout.tsx b/apps/web/src/app/[locale]/layout.tsx new file mode 100644 index 0000000000..ccf44510cd --- /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} + + ); +} 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/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 473607979c..3c0a1fd30f 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 }; @@ -37,6 +38,8 @@ 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, title: page?.title, @@ -46,7 +49,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 +59,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 72% rename from apps/web/src/app/status-page/[domain]/layout.tsx rename to apps/web/src/app/[locale]/status-page/[domain]/layout.tsx index f88b1a0177..1ec636f0b2 100644 --- a/apps/web/src/app/status-page/[domain]/layout.tsx +++ b/apps/web/src/app/[locale]/status-page/[domain]/layout.tsx @@ -1,17 +1,22 @@ import { Shell } from "@/components/dashboard/shell"; import NavigationLink from "./_components/navigation-link"; +import { getI18n } from '@/yuzu/server'; +import { Switcher } from '@/components/status-page/switcher'; -export default function StatusPageLayout({ +export default async function StatusPageLayout({ children, }: { children: React.ReactNode; }) { + + const t = await getI18n() + return (
- Status - Incidents + {t('Status')} + {t('Incidents')}
@@ -21,7 +26,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 100% rename from apps/web/src/app/status-page/[domain]/page.tsx rename to apps/web/src/app/[locale]/status-page/[domain]/page.tsx diff --git a/apps/web/src/components/status-page/incident-list.tsx b/apps/web/src/components/status-page/incident-list.tsx index 80a828b970..bb12e8b945 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", @@ -33,6 +34,8 @@ export const IncidentList = ({ const _incidents = context === "all" ? incidents : getLastWeeksIncidents(); + const t = await getI18n() + return ( <> {_incidents.sort((a, b) => { @@ -42,7 +45,7 @@ export const IncidentList = ({ })?.length > 0 ? (

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

{_incidents.map((incident) => { return ( @@ -53,7 +56,7 @@ export const IncidentList = ({

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

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

@@ -79,8 +82,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 26482bfec1..f3be7d7e4c 100644 --- a/apps/web/src/components/status-page/monitor.tsx +++ b/apps/web/src/components/status-page/monitor.tsx @@ -4,17 +4,20 @@ 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, }: { monitor: z.infer; }) => { + const t = await getI18n() const data = await getMonitorListData({ monitorId: String(monitor.id), groupBy: "day", }); - if (!data) return
Something went wrong
; + + if (!data) return
{t('Something went wrong')}
; return ( + +
+ ) +} \ No newline at end of file diff --git a/apps/web/src/components/tracker.tsx b/apps/web/src/components/tracker.tsx index b417af858a..a2158a866c 100644 --- a/apps/web/src/components/tracker.tsx +++ b/apps/web/src/components/tracker.tsx @@ -22,6 +22,7 @@ import { } from "@/components/ui/tooltip"; import useWindowSize from "@/hooks/use-window-size"; import { blacklistDates, getMonitorList, getStatus } from "@/lib/tracker"; +import { useI18n } from '@/yuzu/client'; // What would be cool is tracker that turn from green to red depending on the number of errors const tracker = cva("h-10 rounded-full flex-1", { @@ -63,6 +64,8 @@ export function Tracker({ context, }); + const t = useI18n() + return (
@@ -72,7 +75,7 @@ export function Tracker({ ) : null}
-

{uptime}% uptime

+

{uptime}% {t('uptime')}

diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 1accb3cd59..d625bc06a6 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -1,12 +1,19 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { authMiddleware, redirectToSignIn } from "@clerk/nextjs"; +import { createI18nMiddleware } from 'next-international/middleware' import { db, eq } from "@openstatus/db"; import { user, usersToWorkspaces, workspace } from "@openstatus/db/src/schema"; 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(); @@ -78,6 +85,13 @@ 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 }); @@ -164,4 +178,4 @@ export const config = { "/(api/webhook|api/trpc)(.*)", "/(!api/checker/:path*|!api/og|!api/ping)", ], -}; +}; \ No newline at end of file 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..eac609d09d --- /dev/null +++ b/apps/web/src/yuzu/de.json @@ -0,0 +1 @@ +{"Status":"Status","Incidents":"Vorfälle","powered by":"angetrieben von","The":"Die","status page":"Statusseite","uptime":"Betriebszeit","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."} \ 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..266e351803 --- /dev/null +++ b/apps/web/src/yuzu/en.json @@ -0,0 +1 @@ +{"Status":"Status","Incidents":"Incidents","powered by":"powered by","The":"The","status page":"status page","uptime":"uptime","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."} \ 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..103d32d670 --- /dev/null +++ b/apps/web/src/yuzu/es.json @@ -0,0 +1 @@ +{"Status":"Estado","Incidents":"Incidentes","powered by":"accionado por","The":"La","status page":"página de estado","uptime":"tiempo de actividad","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."} \ 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..f707c2ce1b --- /dev/null +++ b/apps/web/src/yuzu/fr.json @@ -0,0 +1 @@ +{"Status":"Statut","Incidents":"Incidents","powered by":"alimenté par","The":"La","status page":"page d'état","uptime":"temps de fonctionnement","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."} \ 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..d31d105f36 --- /dev/null +++ b/apps/web/yuzu.config.ts @@ -0,0 +1,24 @@ +export default { + defaultLocale: 'en', + locales: [{ + code: 'en', + name: 'English', + }, { + code: 'es', + name: 'Español', + }, { + code: 'fr', + name: 'Français', + }, { + code: 'de', + name: 'Deutsch', + }], + resources: 'src/yuzu', + content: [ + 'src/app/**/*.{tsx,ts,jsx,js}', + 'src/components/**/*.{tsx,ts,jsx,js}', + ], + transformers: ['t', 'scopedT', 'yuzu', 'yz', 'i18n', ''], + framework: 'nextjs', + tsx: true, +} as const \ No newline at end of file