From ccfea576a91c92a7242f1c5338876e2a744ce1a8 Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Sat, 7 Mar 2026 18:34:34 +0530 Subject: [PATCH 01/24] feat(status-page): install next-intl 4.5+ and create i18n config Co-Authored-By: Claude Opus 4.6 --- apps/status-page/messages/en.po | 2 + apps/status-page/package.json | 1 + apps/status-page/src/i18n/config.ts | 3 + apps/status-page/src/i18n/request.ts | 15 ++ apps/status-page/src/i18n/routing.ts | 8 + pnpm-lock.yaml | 324 ++++++++++++++++++++++++--- 6 files changed, 321 insertions(+), 32 deletions(-) create mode 100644 apps/status-page/messages/en.po create mode 100644 apps/status-page/src/i18n/config.ts create mode 100644 apps/status-page/src/i18n/request.ts create mode 100644 apps/status-page/src/i18n/routing.ts diff --git a/apps/status-page/messages/en.po b/apps/status-page/messages/en.po new file mode 100644 index 0000000000..8c1ebce251 --- /dev/null +++ b/apps/status-page/messages/en.po @@ -0,0 +1,2 @@ +# English translations (auto-extracted by next-intl) +# This file is auto-generated. Do not edit manually. diff --git a/apps/status-page/package.json b/apps/status-page/package.json index e95de070de..730850e879 100644 --- a/apps/status-page/package.json +++ b/apps/status-page/package.json @@ -53,6 +53,7 @@ "lucide-react": "0.525.0", "next": "16.1.6", "next-auth": "5.0.0-beta.29", + "next-intl": "^4.8.3", "next-plausible": "3.12.5", "next-themes": "0.4.6", "nuqs": "2.8.5", diff --git a/apps/status-page/src/i18n/config.ts b/apps/status-page/src/i18n/config.ts new file mode 100644 index 0000000000..f555d0e8f5 --- /dev/null +++ b/apps/status-page/src/i18n/config.ts @@ -0,0 +1,3 @@ +export const locales = ["en", "es", "fr", "de", "pt", "ja", "zh", "ko"] as const; +export type Locale = (typeof locales)[number]; +export const defaultLocale: Locale = "en"; diff --git a/apps/status-page/src/i18n/request.ts b/apps/status-page/src/i18n/request.ts new file mode 100644 index 0000000000..54f8e68604 --- /dev/null +++ b/apps/status-page/src/i18n/request.ts @@ -0,0 +1,15 @@ +import { getRequestConfig } from "next-intl/server"; +import { routing } from "./routing"; + +export default getRequestConfig(async ({ requestLocale }) => { + let locale = await requestLocale; + + if (!locale || !routing.locales.includes(locale as any)) { + locale = routing.defaultLocale; + } + + return { + locale, + messages: (await import(`../../messages/${locale}.po`)).default, + }; +}); diff --git a/apps/status-page/src/i18n/routing.ts b/apps/status-page/src/i18n/routing.ts new file mode 100644 index 0000000000..1e30e8a9e8 --- /dev/null +++ b/apps/status-page/src/i18n/routing.ts @@ -0,0 +1,8 @@ +import { defineRouting } from "next-intl/routing"; +import { locales, defaultLocale } from "./config"; + +export const routing = defineRouting({ + locales, + defaultLocale, + localePrefix: "as-needed", +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15c56da8ff..0d38e621ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 1.8.3 '@turbo/gen': specifier: 1.13.3 - version: 1.13.3(@types/node@24.0.8)(typescript@5.9.3) + version: 1.13.3(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@24.0.8)(typescript@5.9.3) '@types/node': specifier: 24.0.8 version: 24.0.8 @@ -178,7 +178,7 @@ importers: version: 1.2.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@sentry/nextjs': specifier: 10.31.0 - version: 10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0) + version: 10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0(@swc/core@1.15.18(@swc/helpers@0.5.17))) '@stripe/stripe-js': specifier: 2.1.6 version: 2.1.6 @@ -226,7 +226,7 @@ importers: version: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-auth: specifier: 5.0.0-beta.29 - version: 5.0.0-beta.29(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 5.0.0-beta.29(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) next-themes: specifier: 0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -650,7 +650,7 @@ importers: version: 1.1.14(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@sentry/nextjs': specifier: 10.31.0 - version: 10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0) + version: 10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0(@swc/core@1.15.18(@swc/helpers@0.5.17))) '@stripe/stripe-js': specifier: 2.1.6 version: 2.1.6 @@ -698,7 +698,10 @@ importers: version: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-auth: specifier: 5.0.0-beta.29 - version: 5.0.0-beta.29(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 5.0.0-beta.29(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + next-intl: + specifier: ^4.8.3 + version: 4.8.3(@swc/helpers@0.5.17)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(typescript@5.9.3) next-plausible: specifier: 3.12.5 version: 3.12.5(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -879,7 +882,7 @@ importers: version: 1.2.3(@types/react@19.2.2)(react@19.2.3) '@sentry/nextjs': specifier: 10.31.0 - version: 10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0) + version: 10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0(@swc/core@1.15.18(@swc/helpers@0.5.17))) '@stripe/stripe-js': specifier: 2.1.6 version: 2.1.6 @@ -957,7 +960,7 @@ importers: version: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-auth: specifier: 5.0.0-beta.29 - version: 5.0.0-beta.29(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 5.0.0-beta.29(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) next-mdx-remote: specifier: 6.0.0 version: 6.0.0(@types/react@19.2.2)(react@19.2.3) @@ -1365,7 +1368,7 @@ importers: version: 0.31.4 next-auth: specifier: 5.0.0-beta.29 - version: 5.0.0-beta.29(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 5.0.0-beta.29(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -1921,7 +1924,7 @@ importers: version: 4.1.8 tsup: specifier: 7.2.0 - version: 7.2.0(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.9.3))(typescript@5.9.3) + version: 7.2.0(@swc/core@1.15.18(@swc/helpers@0.5.17))(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@20.8.0)(typescript@5.9.3))(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -2215,7 +2218,7 @@ importers: version: 22.10.2 tsup: specifier: 7.2.0 - version: 7.2.0(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.9.3))(typescript@5.9.3) + version: 7.2.0(@swc/core@1.15.18(@swc/helpers@0.5.17))(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@22.10.2)(typescript@5.9.3))(typescript@5.9.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -3399,6 +3402,21 @@ packages: '@fontsource-variable/inter@5.2.8': resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} + '@formatjs/ecma402-abstract@3.1.1': + resolution: {integrity: sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==} + + '@formatjs/fast-memoize@3.1.0': + resolution: {integrity: sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==} + + '@formatjs/icu-messageformat-parser@3.5.1': + resolution: {integrity: sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==} + + '@formatjs/icu-skeleton-parser@2.1.1': + resolution: {integrity: sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==} + + '@formatjs/intl-localematcher@0.8.1': + resolution: {integrity: sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==} + '@google-cloud/tasks@4.0.1': resolution: {integrity: sha512-cluHSN52WgaNoDPVguxCeXZq4rTBHqJntXFB9aI9zG8n8MJukf6V0H7yoAXpKXQxyeXv4LRy108kAgLPgXP3yA==} engines: {node: '>=14.0.0'} @@ -6268,6 +6286,9 @@ packages: resolution: {integrity: sha512-fNcaZbZKoZ2PvoW+KJHmk4au8ZukgWlb6qLK3k/SLkfsTggN3DO4PR57ch6cyl2WhwENNbw+iI+ss7fTRcPnOA==} engines: {node: '>=18'} + '@schummar/icu-type-parser@1.21.5': + resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -6664,12 +6685,87 @@ packages: '@stripe/stripe-js@2.1.6': resolution: {integrity: sha512-QSzqQIcowgap7a40f3a7oUR+59Xet/i8fp1EsnzzwxK5oPRQsCbbLQ4Cd6qM0y1pdZMonFnCrAWayWdE9Lr0iA==} + '@swc/core-darwin-arm64@1.15.18': + resolution: {integrity: sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.18': + resolution: {integrity: sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.18': + resolution: {integrity: sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.18': + resolution: {integrity: sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.18': + resolution: {integrity: sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.18': + resolution: {integrity: sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.18': + resolution: {integrity: sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.18': + resolution: {integrity: sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.18': + resolution: {integrity: sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.18': + resolution: {integrity: sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.18': + resolution: {integrity: sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + '@swc/types@0.1.25': + resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + '@t3-oss/env-core@0.13.10': resolution: {integrity: sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g==} peerDependencies: @@ -8068,6 +8164,9 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} @@ -9045,6 +9144,9 @@ packages: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + icu-minify@4.8.3: + resolution: {integrity: sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -9091,6 +9193,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + intl-messageformat@11.1.2: + resolution: {integrity: sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==} + ip-address@10.0.1: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} @@ -9934,6 +10039,19 @@ packages: nodemailer: optional: true + next-intl-swc-plugin-extractor@4.8.3: + resolution: {integrity: sha512-YcaT+R9z69XkGhpDarVFWUprrCMbxgIQYPUaXoE6LGVnLjGdo8hu3gL6bramDVjNKViYY8a/pXPy7Bna0mXORg==} + + next-intl@4.8.3: + resolution: {integrity: sha512-PvdBDWg+Leh7BR7GJUQbCDVVaBRn37GwDBWc9sv0rVQOJDQ5JU1rVzx9EEGuOGYo0DHAl70++9LQ7HxTawdL7w==} + peerDependencies: + next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + next-mdx-remote@6.0.0: resolution: {integrity: sha512-cJEpEZlgD6xGjB4jL8BnI8FaYdN9BzZM4NwadPe1YQr7pqoWjg9EBCMv3nXBkuHqMRfv2y33SzUsuyNh9LFAQQ==} engines: {node: '>=14', npm: '>=7'} @@ -10375,6 +10493,9 @@ packages: engines: {node: '>=18'} hasBin: true + po-parser@2.1.1: + resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==} + postcss-load-config@4.0.2: resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} engines: {node: '>= 14'} @@ -11812,6 +11933,11 @@ packages: peerDependencies: react: '>=16.13' + use-intl@4.8.3: + resolution: {integrity: sha512-nLxlC/RH+le6g3amA508Itnn/00mE+J22ui21QhOWo5V9hCEC43+WtnRAITbJW0ztVZphev5X9gvOf2/Dk9PLA==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + use-sidecar@1.1.3: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} @@ -13766,6 +13892,33 @@ snapshots: '@fontsource-variable/inter@5.2.8': {} + '@formatjs/ecma402-abstract@3.1.1': + dependencies: + '@formatjs/fast-memoize': 3.1.0 + '@formatjs/intl-localematcher': 0.8.1 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@3.1.0': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@3.5.1': + dependencies: + '@formatjs/ecma402-abstract': 3.1.1 + '@formatjs/icu-skeleton-parser': 2.1.1 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@2.1.1': + dependencies: + '@formatjs/ecma402-abstract': 3.1.1 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.8.1': + dependencies: + '@formatjs/fast-memoize': 3.1.0 + tslib: 2.8.1 + '@google-cloud/tasks@4.0.1': dependencies: google-gax: 4.6.1 @@ -16624,6 +16777,8 @@ snapshots: type-fest: 4.41.0 zod: 3.25.76 + '@schummar/icu-type-parser@1.21.5': {} + '@sec-ant/readable-stream@0.4.1': {} '@selderee/plugin-htmlparser2@0.11.0': @@ -16724,7 +16879,7 @@ snapshots: '@sentry/types': 8.9.2 '@sentry/utils': 8.9.2 - '@sentry/nextjs@10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0)': + '@sentry/nextjs@10.31.0(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0(@swc/core@1.15.18(@swc/helpers@0.5.17)))': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 @@ -16736,7 +16891,7 @@ snapshots: '@sentry/opentelemetry': 10.31.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0) '@sentry/react': 10.31.0(react@19.2.3) '@sentry/vercel-edge': 10.31.0 - '@sentry/webpack-plugin': 4.6.1(webpack@5.103.0) + '@sentry/webpack-plugin': 4.6.1(webpack@5.103.0(@swc/core@1.15.18(@swc/helpers@0.5.17))) next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) resolve: 1.22.8 rollup: 4.53.3 @@ -16834,12 +16989,12 @@ snapshots: '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@sentry/core': 10.31.0 - '@sentry/webpack-plugin@4.6.1(webpack@5.103.0)': + '@sentry/webpack-plugin@4.6.1(webpack@5.103.0(@swc/core@1.15.18(@swc/helpers@0.5.17)))': dependencies: '@sentry/bundler-plugin-core': 4.6.1 unplugin: 1.0.1 uuid: 9.0.1 - webpack: 5.103.0 + webpack: 5.103.0(@swc/core@1.15.18(@swc/helpers@0.5.17)) transitivePeerDependencies: - encoding - supports-color @@ -17228,6 +17383,55 @@ snapshots: '@stripe/stripe-js@2.1.6': {} + '@swc/core-darwin-arm64@1.15.18': + optional: true + + '@swc/core-darwin-x64@1.15.18': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.18': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.18': + optional: true + + '@swc/core-linux-arm64-musl@1.15.18': + optional: true + + '@swc/core-linux-x64-gnu@1.15.18': + optional: true + + '@swc/core-linux-x64-musl@1.15.18': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.18': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.18': + optional: true + + '@swc/core-win32-x64-msvc@1.15.18': + optional: true + + '@swc/core@1.15.18(@swc/helpers@0.5.17)': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.25 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.18 + '@swc/core-darwin-x64': 1.15.18 + '@swc/core-linux-arm-gnueabihf': 1.15.18 + '@swc/core-linux-arm64-gnu': 1.15.18 + '@swc/core-linux-arm64-musl': 1.15.18 + '@swc/core-linux-x64-gnu': 1.15.18 + '@swc/core-linux-x64-musl': 1.15.18 + '@swc/core-win32-arm64-msvc': 1.15.18 + '@swc/core-win32-ia32-msvc': 1.15.18 + '@swc/core-win32-x64-msvc': 1.15.18 + '@swc/helpers': 0.5.17 + + '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -17236,6 +17440,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@swc/types@0.1.25': + dependencies: + '@swc/counter': 0.1.3 + '@t3-oss/env-core@0.13.10(typescript@5.9.3)(zod@4.1.13)': optionalDependencies: typescript: 5.9.3 @@ -17499,7 +17707,7 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@turbo/gen@1.13.3(@types/node@24.0.8)(typescript@5.9.3)': + '@turbo/gen@1.13.3(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@24.0.8)(typescript@5.9.3)': dependencies: '@turbo/workspaces': 1.13.3(@types/node@24.0.8) chalk: 2.4.2 @@ -17509,7 +17717,7 @@ snapshots: minimatch: 9.0.5 node-plop: 0.26.3 proxy-agent: 6.5.0 - ts-node: 10.9.2(@types/node@24.0.8)(typescript@5.9.3) + ts-node: 10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@24.0.8)(typescript@5.9.3) update-check: 1.5.4 validate-npm-package-name: 5.0.1 transitivePeerDependencies: @@ -18737,6 +18945,8 @@ snapshots: decimal.js-light@2.5.1: {} + decimal.js@10.6.0: {} + decode-named-character-reference@1.2.0: dependencies: character-entities: 2.0.2 @@ -19937,6 +20147,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + icu-minify@4.8.3: + dependencies: + '@formatjs/icu-messageformat-parser': 3.5.1 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -20006,6 +20220,13 @@ snapshots: internmap@2.0.3: {} + intl-messageformat@11.1.2: + dependencies: + '@formatjs/ecma402-abstract': 3.1.1 + '@formatjs/fast-memoize': 3.1.0 + '@formatjs/icu-messageformat-parser': 3.5.1 + tslib: 2.8.1 + ip-address@10.0.1: {} ip-address@10.1.0: {} @@ -21023,12 +21244,31 @@ snapshots: netmask@2.0.2: {} - next-auth@5.0.0-beta.29(next@16.1.6(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): + next-auth@5.0.0-beta.29(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): dependencies: '@auth/core': 0.40.0 next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 + next-intl-swc-plugin-extractor@4.8.3: {} + + next-intl@4.8.3(@swc/helpers@0.5.17)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(typescript@5.9.3): + dependencies: + '@formatjs/intl-localematcher': 0.8.1 + '@parcel/watcher': 2.5.1 + '@swc/core': 1.15.18(@swc/helpers@0.5.17) + icu-minify: 4.8.3 + negotiator: 1.0.0 + next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next-intl-swc-plugin-extractor: 4.8.3 + po-parser: 2.1.1 + react: 19.2.3 + use-intl: 4.8.3(react@19.2.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@swc/helpers' + next-mdx-remote@6.0.0(@types/react@19.2.2)(react@19.2.3): dependencies: '@babel/code-frame': 7.29.0 @@ -21541,21 +21781,23 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.9.3)): + po-parser@2.1.1: {} + + postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@20.8.0)(typescript@5.9.3)): dependencies: lilconfig: 3.1.3 yaml: 2.8.1 optionalDependencies: postcss: 8.5.6 - ts-node: 10.9.2(@types/node@20.8.0)(typescript@5.9.3) + ts-node: 10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@20.8.0)(typescript@5.9.3) - postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.9.3)): + postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@22.10.2)(typescript@5.9.3)): dependencies: lilconfig: 3.1.3 yaml: 2.8.1 optionalDependencies: postcss: 8.5.6 - ts-node: 10.9.2(@types/node@22.10.2)(typescript@5.9.3) + ts-node: 10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@22.10.2)(typescript@5.9.3) postcss-nested@6.2.0(postcss@8.4.38): dependencies: @@ -22954,14 +23196,16 @@ snapshots: ansi-escapes: 7.2.0 supports-hyperlinks: 4.4.0 - terser-webpack-plugin@5.3.16(webpack@5.103.0): + terser-webpack-plugin@5.3.16(@swc/core@1.15.18(@swc/helpers@0.5.17))(webpack@5.103.0(@swc/core@1.15.18(@swc/helpers@0.5.17))): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.103.0 + webpack: 5.103.0(@swc/core@1.15.18(@swc/helpers@0.5.17)) + optionalDependencies: + '@swc/core': 1.15.18(@swc/helpers@0.5.17) terser@5.44.1: dependencies: @@ -23052,7 +23296,7 @@ snapshots: '@ts-morph/common': 0.27.0 code-block-writer: 13.0.3 - ts-node@10.9.2(@types/node@20.8.0)(typescript@5.9.3): + ts-node@10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@20.8.0)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 @@ -23069,9 +23313,11 @@ snapshots: typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.18(@swc/helpers@0.5.17) optional: true - ts-node@10.9.2(@types/node@22.10.2)(typescript@5.9.3): + ts-node@10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@22.10.2)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 @@ -23088,9 +23334,11 @@ snapshots: typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.18(@swc/helpers@0.5.17) optional: true - ts-node@10.9.2(@types/node@24.0.8)(typescript@5.9.3): + ts-node@10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@24.0.8)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 @@ -23107,6 +23355,8 @@ snapshots: typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.18(@swc/helpers@0.5.17) tsconfck@3.1.6(typescript@5.9.3): optionalDependencies: @@ -23122,7 +23372,7 @@ snapshots: tslib@2.8.1: {} - tsup@7.2.0(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.9.3))(typescript@5.9.3): + tsup@7.2.0(@swc/core@1.15.18(@swc/helpers@0.5.17))(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@20.8.0)(typescript@5.9.3))(typescript@5.9.3): dependencies: bundle-require: 4.2.1(esbuild@0.18.20) cac: 6.7.14 @@ -23132,20 +23382,21 @@ snapshots: execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.9.3)) + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@20.8.0)(typescript@5.9.3)) resolve-from: 5.0.0 rollup: 3.29.5 source-map: 0.8.0-beta.0 sucrase: 3.35.1 tree-kill: 1.2.2 optionalDependencies: + '@swc/core': 1.15.18(@swc/helpers@0.5.17) postcss: 8.5.6 typescript: 5.9.3 transitivePeerDependencies: - supports-color - ts-node - tsup@7.2.0(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.9.3))(typescript@5.9.3): + tsup@7.2.0(@swc/core@1.15.18(@swc/helpers@0.5.17))(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@22.10.2)(typescript@5.9.3))(typescript@5.9.3): dependencies: bundle-require: 4.2.1(esbuild@0.18.20) cac: 6.7.14 @@ -23155,13 +23406,14 @@ snapshots: execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.9.3)) + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.18(@swc/helpers@0.5.17))(@types/node@22.10.2)(typescript@5.9.3)) resolve-from: 5.0.0 rollup: 3.29.5 source-map: 0.8.0-beta.0 sucrase: 3.35.1 tree-kill: 1.2.2 optionalDependencies: + '@swc/core': 1.15.18(@swc/helpers@0.5.17) postcss: 8.5.6 typescript: 5.9.3 transitivePeerDependencies: @@ -23432,6 +23684,14 @@ snapshots: dequal: 2.0.3 react: 19.2.3 + use-intl@4.8.3(react@19.2.3): + dependencies: + '@formatjs/fast-memoize': 3.1.0 + '@schummar/icu-type-parser': 1.21.5 + icu-minify: 4.8.3 + intl-messageformat: 11.1.2 + react: 19.2.3 + use-sidecar@1.1.3(@types/react@19.2.2)(react@19.2.3): dependencies: detect-node-es: 1.1.0 @@ -23653,7 +23913,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.103.0: + webpack@5.103.0(@swc/core@1.15.18(@swc/helpers@0.5.17)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -23677,7 +23937,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(webpack@5.103.0) + terser-webpack-plugin: 5.3.16(@swc/core@1.15.18(@swc/helpers@0.5.17))(webpack@5.103.0(@swc/core@1.15.18(@swc/helpers@0.5.17))) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: From 90feb8e3c6e64cd01f8b3a505b60be2e21261266 Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Sat, 7 Mar 2026 18:35:12 +0530 Subject: [PATCH 02/24] feat(status-page): add next-intl plugin to next.config.ts Co-Authored-By: Claude Opus 4.6 --- apps/status-page/next.config.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/status-page/next.config.ts b/apps/status-page/next.config.ts index 9ff1620b62..d2aaa45877 100644 --- a/apps/status-page/next.config.ts +++ b/apps/status-page/next.config.ts @@ -1,7 +1,9 @@ import { withSentryConfig } from "@sentry/nextjs"; - +import createNextIntlPlugin from "next-intl/plugin"; import type { NextConfig } from "next"; +const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); + const nextConfig: NextConfig = { output: process.env.SELF_HOST === "true" ? "standalone" : undefined, experimental: { @@ -79,4 +81,4 @@ const sentryConfig = { }, }; -export default withSentryConfig(nextConfig, sentryConfig); +export default withSentryConfig(withNextIntl(nextConfig), sentryConfig); From 4e687684ca8cf1325e73d6f1e9a20798a8f5c5d7 Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Sat, 7 Mar 2026 18:36:11 +0530 Subject: [PATCH 03/24] feat(status-page): add [locale] route segment and move routes Co-Authored-By: Claude Opus 4.6 --- .../[domain]/{ => [locale]}/(auth)/layout.tsx | 0 .../login/_components/section-magic-link.tsx | 0 .../login/_components/section-password.tsx | 0 .../{ => [locale]}/(auth)/login/actions.ts | 0 .../{ => [locale]}/(auth)/login/page.tsx | 0 .../{ => [locale]}/(public)/badge/route.tsx | 0 .../{ => [locale]}/(public)/badge/v2/route.ts | 0 .../(public)/events/(list)/page.tsx | 0 .../(public)/events/(list)/search-params.ts | 0 .../events/(view)/maintenance/[id]/layout.tsx | 0 .../events/(view)/maintenance/[id]/page.tsx | 0 .../events/(view)/report/[id]/layout.tsx | 0 .../events/(view)/report/[id]/page.tsx | 0 .../{ => [locale]}/(public)/events/layout.tsx | 0 .../(public)/feed/[type]/route.ts | 0 .../(public)/feed/json/route.ts | 0 .../{ => [locale]}/(public)/layout.tsx | 0 .../(public)/manage/[token]/layout.tsx | 0 .../(public)/manage/[token]/page.tsx | 0 .../{ => [locale]}/(public)/manage/layout.tsx | 0 .../(public)/monitors/[id]/page.tsx | 0 .../(public)/monitors/[id]/search-params.ts | 0 .../{ => [locale]}/(public)/monitors/page.tsx | 0 .../[domain]/{ => [locale]}/(public)/page.tsx | 0 .../(public)/unsubscribe/[token]/layout.tsx | 0 .../(public)/unsubscribe/[token]/page.tsx | 0 .../(public)/verify/[token]/layout.tsx | 0 .../(public)/verify/[token]/page.tsx | 0 .../[domain]/[locale]/layout.tsx | 28 +++++++++++++++++++ 29 files changed, 28 insertions(+) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(auth)/layout.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(auth)/login/_components/section-magic-link.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(auth)/login/_components/section-password.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(auth)/login/actions.ts (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(auth)/login/page.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/badge/route.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/badge/v2/route.ts (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/events/(list)/page.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/events/(list)/search-params.ts (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/events/(view)/maintenance/[id]/layout.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/events/(view)/maintenance/[id]/page.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/events/(view)/report/[id]/layout.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/events/(view)/report/[id]/page.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/events/layout.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/feed/[type]/route.ts (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/feed/json/route.ts (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/layout.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/manage/[token]/layout.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/manage/[token]/page.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/manage/layout.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/monitors/[id]/page.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/monitors/[id]/search-params.ts (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/monitors/page.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/page.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/unsubscribe/[token]/layout.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/unsubscribe/[token]/page.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/verify/[token]/layout.tsx (100%) rename apps/status-page/src/app/(status-page)/[domain]/{ => [locale]}/(public)/verify/[token]/page.tsx (100%) create mode 100644 apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(auth)/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/layout.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(auth)/layout.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/layout.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(auth)/login/_components/section-magic-link.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-magic-link.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(auth)/login/_components/section-magic-link.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-magic-link.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(auth)/login/_components/section-password.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-password.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(auth)/login/_components/section-password.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-password.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(auth)/login/actions.ts b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/actions.ts similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(auth)/login/actions.ts rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/actions.ts diff --git a/apps/status-page/src/app/(status-page)/[domain]/(auth)/login/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/page.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(auth)/login/page.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/page.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/badge/route.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/badge/route.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/badge/route.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/badge/route.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/badge/v2/route.ts b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/badge/v2/route.ts similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/badge/v2/route.ts rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/badge/v2/route.ts diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/events/(list)/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(list)/page.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/events/(list)/page.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(list)/page.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/events/(list)/search-params.ts b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(list)/search-params.ts similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/events/(list)/search-params.ts rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(list)/search-params.ts diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/maintenance/[id]/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/maintenance/[id]/layout.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/maintenance/[id]/layout.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/maintenance/[id]/layout.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/maintenance/[id]/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/maintenance/[id]/page.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/maintenance/[id]/page.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/maintenance/[id]/page.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/report/[id]/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/report/[id]/layout.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/report/[id]/layout.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/report/[id]/layout.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/report/[id]/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/report/[id]/page.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/report/[id]/page.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/report/[id]/page.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/events/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/layout.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/events/layout.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/layout.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/feed/[type]/route.ts b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/feed/[type]/route.ts similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/feed/[type]/route.ts rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/feed/[type]/route.ts diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/feed/json/route.ts b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/feed/json/route.ts similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/feed/json/route.ts rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/feed/json/route.ts diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/layout.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/layout.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/layout.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/manage/[token]/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/manage/[token]/layout.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/manage/[token]/layout.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/manage/[token]/layout.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/manage/[token]/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/manage/[token]/page.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/manage/[token]/page.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/manage/[token]/page.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/manage/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/manage/layout.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/manage/layout.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/manage/layout.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/monitors/[id]/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/monitors/[id]/page.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/monitors/[id]/search-params.ts b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/search-params.ts similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/monitors/[id]/search-params.ts rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/search-params.ts diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/monitors/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/page.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/monitors/page.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/page.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/page.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/page.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/page.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/unsubscribe/[token]/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/unsubscribe/[token]/layout.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/unsubscribe/[token]/layout.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/unsubscribe/[token]/layout.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/unsubscribe/[token]/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/unsubscribe/[token]/page.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/unsubscribe/[token]/page.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/unsubscribe/[token]/page.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/verify/[token]/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/verify/[token]/layout.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/verify/[token]/layout.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/verify/[token]/layout.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/(public)/verify/[token]/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/verify/[token]/page.tsx similarity index 100% rename from apps/status-page/src/app/(status-page)/[domain]/(public)/verify/[token]/page.tsx rename to apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/verify/[token]/page.tsx diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx new file mode 100644 index 0000000000..2976c9a708 --- /dev/null +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx @@ -0,0 +1,28 @@ +import { NextIntlClientProvider, hasLocale } from "next-intl"; +import { setRequestLocale } from "next-intl/server"; +import { notFound } from "next/navigation"; +import { routing } from "@/i18n/routing"; + +export default async function LocaleLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + + if (!hasLocale(routing.locales, locale)) { + notFound(); + } + + setRequestLocale(locale); + + const messages = (await import(`@/../../messages/${locale}.po`)).default; + + return ( + + {children} + + ); +} From 882ac924fb8daa13283d3777dd3297d18a0556a3 Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Thu, 12 Mar 2026 21:35:08 +0530 Subject: [PATCH 04/24] feat(status-page): use useExtracted for status component strings Replace hardcoded strings in status-banner, status-monitor, status-events, and status-feed with useExtracted() i18n calls. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Moulik Aggarwal --- .../components/status-page/status-banner.tsx | 13 ++++++++----- .../components/status-page/status-events.tsx | 18 ++++++++++++++---- .../src/components/status-page/status-feed.tsx | 10 ++++++---- .../components/status-page/status-monitor.tsx | 13 ++++++++----- 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/apps/status-page/src/components/status-page/status-banner.tsx b/apps/status-page/src/components/status-page/status-banner.tsx index ffce8171f3..4fd66d12ce 100644 --- a/apps/status-page/src/components/status-page/status-banner.tsx +++ b/apps/status-page/src/components/status-page/status-banner.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Tabs, TabsContent, @@ -11,7 +13,7 @@ import { TriangleAlertIcon, WrenchIcon, } from "lucide-react"; -import { messages } from "./messages"; +import { useExtracted } from "next-intl"; import { StatusTimestamp } from "./status"; export function StatusBanner({ @@ -70,19 +72,20 @@ export function StatusBannerMessage({ className, ...props }: React.ComponentProps<"div">) { + const t = useExtracted(); return (
- {messages.long.success} + {t("All Systems Operational")} - {messages.long.degraded} + {t("Degraded Performance")} - {messages.long.error} + {t("Downtime Performance")} - {messages.long.info} + {t("Maintenance")}
); diff --git a/apps/status-page/src/components/status-page/status-events.tsx b/apps/status-page/src/components/status-page/status-events.tsx index 5ef261eb06..e9fbdcd958 100644 --- a/apps/status-page/src/components/status-page/status-events.tsx +++ b/apps/status-page/src/components/status-page/status-events.tsx @@ -13,7 +13,7 @@ import { import { cn } from "@openstatus/ui/lib/utils"; import { formatDistanceStrict } from "date-fns"; import { Check } from "lucide-react"; -import { status } from "./messages"; +import { useExtracted } from "next-intl"; export function StatusEventGroup({ className, @@ -80,6 +80,7 @@ export function StatusEventTitleCheck({ children, ...props }: React.ComponentProps<"div">) { + const t = useExtracted(); return (
@@ -90,7 +91,7 @@ export function StatusEventTitleCheck({
-

Report resolved

+

{t("Report resolved")}

@@ -249,6 +250,14 @@ export function StatusEventTimelineReportUpdate({ withDot?: boolean; isLast?: boolean; }) { + const t = useExtracted(); + const statusLabels = { + resolved: t("Resolved"), + monitoring: t("Monitoring"), + identified: t("Identified"), + investigating: t("Investigating"), + } as const; + return (
@@ -263,7 +272,7 @@ export function StatusEventTimelineReportUpdate({ ) : null}
- {status[report.status]}{" "} + {statusLabels[report.status]}{" "} ·{" "} @@ -302,6 +311,7 @@ export function StatusEventTimelineMaintenance({ }; withDot?: boolean; }) { + const t = useExtracted(); const duration = formatDistanceStrict(maintenance.from, maintenance.to); const range = formatDateRange(maintenance.from, maintenance.to); // NOTE: because formatDateRange is sure to return a range, we can split it into two dates @@ -320,7 +330,7 @@ export function StatusEventTimelineMaintenance({ {/* NOTE: is always last, no need for className="mb-2" */}
- Maintenance{" "} + {t("Maintenance")}{" "} ·{" "} diff --git a/apps/status-page/src/components/status-page/status-feed.tsx b/apps/status-page/src/components/status-page/status-feed.tsx index 77058abf8a..874f1fb427 100644 --- a/apps/status-page/src/components/status-page/status-feed.tsx +++ b/apps/status-page/src/components/status-page/status-feed.tsx @@ -1,5 +1,6 @@ "use client"; import { usePathnamePrefix } from "@/hooks/use-pathname-prefix"; +import { useExtracted } from "next-intl"; import Link from "next/link"; import { StatusBlankContainer, @@ -60,6 +61,7 @@ export function StatusFeed({ showLinks?: boolean; }) { const prefix = usePathnamePrefix(); + const t = useExtracted(); const unifiedEvents: UnifiedEvent[] = [ ...statusReports.map((report) => ({ id: report.id, @@ -87,12 +89,12 @@ export function StatusFeed({
- No recent notifications + {t("No recent notifications")} - There have been no reports within the last 7 days. + {t("There have been no reports within the last 7 days.")} - View events history + {t("View events history")} @@ -179,7 +181,7 @@ export function StatusFeed({ className="mx-auto" href={`${prefix ? `/${prefix}` : ""}/events`} > - View events history + {t("View events history")} ); diff --git a/apps/status-page/src/components/status-page/status-monitor.tsx b/apps/status-page/src/components/status-page/status-monitor.tsx index cafc4d51d7..e862249a27 100644 --- a/apps/status-page/src/components/status-page/status-monitor.tsx +++ b/apps/status-page/src/components/status-page/status-monitor.tsx @@ -18,6 +18,7 @@ import { TriangleAlertIcon, WrenchIcon, } from "lucide-react"; +import { useExtracted } from "next-intl"; import { useState } from "react"; import type { VariantType } from "./floating-button"; import { StatusTracker, StatusTrackerSkeleton } from "./status-tracker"; @@ -164,6 +165,7 @@ export function StatusMonitorFooter({ data: Data; isLoading?: boolean; }) { + const t = useExtracted(); return (
@@ -178,7 +180,7 @@ export function StatusMonitorFooter({ "-" )}
-
today
+
{t("today")}
); } @@ -212,6 +214,7 @@ export function StatusMonitorStatus({ className, ...props }: React.ComponentProps<"div">) { + const t = useExtracted(); return (
- Operational + {t("Operational")} - Degraded + {t("Degraded")} - Downtime + {t("Downtime")} - Maintenance + {t("Maintenance")}
); From b9331eec27e795023cf5b409755e0a9b01b0e6c5 Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Sat, 7 Mar 2026 18:43:24 +0530 Subject: [PATCH 05/24] feat(status-page): use useExtracted for navigation strings Co-Authored-By: Claude Opus 4.6 --- .../status-page/src/components/nav/footer.tsx | 4 +++- .../status-page/src/components/nav/header.tsx | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/apps/status-page/src/components/nav/footer.tsx b/apps/status-page/src/components/nav/footer.tsx index 8f390ef7e5..a0b5c1e42d 100644 --- a/apps/status-page/src/components/nav/footer.tsx +++ b/apps/status-page/src/components/nav/footer.tsx @@ -7,10 +7,12 @@ import { useTRPC } from "@/lib/trpc/client"; import { Skeleton } from "@openstatus/ui/components/ui/skeleton"; import { useQuery } from "@tanstack/react-query"; import { Clock } from "lucide-react"; +import { useExtracted } from "next-intl"; import { useParams } from "next/navigation"; import { useEffect, useState } from "react"; export function Footer(props: React.ComponentProps<"footer">) { + const t = useExtracted(); const { domain } = useParams<{ domain: string }>(); const [isMounted, setIsMounted] = useState(false); const trpc = useTRPC(); @@ -31,7 +33,7 @@ export function Footer(props: React.ComponentProps<"footer">) {
{!page.whiteLabel ? (

- powered by{" "} + {t("powered by")}{" "} ) { + const t = useExtracted(); const trpc = useTRPC(); const { domain } = useParams<{ domain: string }>(); const { data: page } = useQuery({ @@ -93,7 +96,7 @@ export function Header(props: React.ComponentProps<"header">) { if (isTRPCClientError(error)) { toast.error(error.message); } else { - toast.error("Failed to subscribe"); + toast.error(t("Failed to subscribe")); } }, }, @@ -188,6 +191,7 @@ function NavMobile({ className, ...props }: React.ComponentProps) { + const t = useExtracted(); const [open, setOpen] = useState(false); const nav = useNav(); return ( @@ -204,7 +208,7 @@ function NavMobile({ - Menu + {t("Menu")}

    @@ -239,6 +243,7 @@ function GetInTouch({ buttonType: "icon" | "text"; link: string; }) { + const t = useExtracted(); if (buttonType === "text") { return ( ); @@ -273,7 +278,7 @@ function GetInTouch({ -

    Get in touch

    +

    {t("Get in touch")}

    From 7321996259d6fe3e4b4f601ea7b50336d5784a9d Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Sat, 7 Mar 2026 18:46:57 +0530 Subject: [PATCH 06/24] feat(status-page): use useExtracted for events, report, maintenance, and monitor pages Co-Authored-By: Claude Opus 4.6 --- .../[locale]/(public)/events/(list)/page.tsx | 10 +++--- .../events/(view)/maintenance/[id]/page.tsx | 6 ++-- .../events/(view)/report/[id]/page.tsx | 6 ++-- .../[locale]/(public)/monitors/[id]/page.tsx | 35 ++++++++----------- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(list)/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(list)/page.tsx index 1b716087a7..40bf667533 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(list)/page.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(list)/page.tsx @@ -22,12 +22,14 @@ import { TabsTrigger, } from "@openstatus/ui/components/ui/tabs"; import { useQuery } from "@tanstack/react-query"; +import { useExtracted } from "next-intl"; import Link from "next/link"; import { useParams } from "next/navigation"; import { useQueryStates } from "nuqs"; import { searchParamsParsers } from "./search-params"; export default function Page() { + const t = useExtracted(); const [{ tab }, setSearchParams] = useQueryStates(searchParamsParsers); const { domain } = useParams<{ domain: string }>(); const trpc = useTRPC(); @@ -48,8 +50,8 @@ export default function Page() { className="gap-4" > - Reports - Maintenances + {t("Reports")} + {t("Maintenances")} @@ -147,8 +149,8 @@ export default function Page() { }) ) : ( )} diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/maintenance/[id]/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/maintenance/[id]/page.tsx index 20cb726716..55dd272d9b 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/maintenance/[id]/page.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/maintenance/[id]/page.tsx @@ -16,9 +16,11 @@ import { StatusEventTimelineMaintenance, StatusEventTitle, } from "@/components/status-page/status-events"; +import { useExtracted } from "next-intl"; import { useParams } from "next/navigation"; export default function MaintenancePage() { + const t = useExtracted(); const trpc = useTRPC(); const { id, domain } = useParams<{ id: string; domain: string }>(); const { data: maintenance } = useQuery( @@ -31,8 +33,8 @@ export default function MaintenancePage() { if (!maintenance) { return ( ); } diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/report/[id]/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/report/[id]/page.tsx index 1cd49cd63a..2d8c00b200 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/report/[id]/page.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/events/(view)/report/[id]/page.tsx @@ -16,9 +16,11 @@ import { } from "@/components/status-page/status-events"; import { useTRPC } from "@/lib/trpc/client"; import { useQuery } from "@tanstack/react-query"; +import { useExtracted } from "next-intl"; import { useParams } from "next/navigation"; export default function ReportPage() { + const t = useExtracted(); const trpc = useTRPC(); const { id, domain } = useParams<{ id: string; domain: string }>(); const { data: report } = useQuery( @@ -28,8 +30,8 @@ export default function ReportPage() { if (!report) { return ( ); } diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx index 37b518c005..51bb06be9a 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx @@ -47,12 +47,14 @@ import { useTRPC } from "@/lib/trpc/client"; import { Badge } from "@openstatus/ui/components/ui/badge"; import { useQuery } from "@tanstack/react-query"; import { TrendingUp } from "lucide-react"; +import { useExtracted } from "next-intl"; import { useParams } from "next/navigation"; import { useQueryStates } from "nuqs"; import { useMemo } from "react"; import { searchParamsParsers } from "./search-params"; export default function Page() { + const t = useExtracted(); const [{ tab }, setSearchParams] = useQueryStates(searchParamsParsers); const trpc = useTRPC(); const { id, domain } = useParams<{ id: string; domain: string }>(); @@ -205,8 +207,8 @@ export default function Page() { if (!isLoading && !monitor) { return ( ); } @@ -231,7 +233,7 @@ export default function Page() { - Global Latency + {t("Global Latency")} {isLoading ? ( @@ -246,13 +248,13 @@ export default function Page() { - Region Latency + {t("Region Latency")} {isLoading ? ( ) : ( - {tempMonitor?.regions.length} regions{" "} + {tempMonitor?.regions.length} {t("regions")}{" "} - Uptime + {t("Uptime")} {isLoading ? ( @@ -272,7 +274,7 @@ export default function Page() { {uptimePercentage}{" "} - {totalChecks} checks + {totalChecks} {t("checks")} )} @@ -281,10 +283,9 @@ export default function Page() { - Global Latency + {t("Global Latency")} - The aggregated latency from all active regions based on - different quantiles. + {t("The aggregated latency from all active regions based on different quantiles.")} {isLoading ? ( @@ -304,15 +305,9 @@ export default function Page() { - Latency by Region + {t("Latency by Region")} - {/* TODO: we could add an information to p95 that it takes the highest selected global latency percentile */} - Region latency per{" "} - p75{" "} - quantile, sorted by slowest - region. Compare up to{" "} - 6{" "} - regions. + {t("Region latency per p75 quantile, sorted by slowest region. Compare up to 6 regions.")} {isLoading ? ( @@ -329,9 +324,9 @@ export default function Page() { - Total Uptime + {t("Total Uptime")} - Main values of uptime and availability, transparent. + {t("Main values of uptime and availability, transparent.")} {isLoading ? ( From 5581722a13bf79c1e9f2de86ca1fdda37a5fbb1d Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Thu, 12 Mar 2026 21:35:46 +0530 Subject: [PATCH 07/24] feat(status-page): use useExtracted for auth, forms, and updates strings Replace hardcoded English strings with useExtracted() calls in login sections, form components, and status updates popover. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Moulik Aggarwal --- .../login/_components/section-magic-link.tsx | 14 ++++--- .../login/_components/section-password.tsx | 8 ++-- .../[domain]/[locale]/(auth)/login/actions.ts | 6 ++- .../src/components/forms/form-email.tsx | 10 +++-- .../forms/form-manage-subscription.tsx | 14 ++++--- .../src/components/forms/form-password.tsx | 10 +++-- .../components/forms/form-subscribe-email.tsx | 16 +++---- .../components/status-page/status-updates.tsx | 42 ++++++++++--------- 8 files changed, 68 insertions(+), 52 deletions(-) diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-magic-link.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-magic-link.tsx index 8399f0dfc3..23b8c30a00 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-magic-link.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-magic-link.tsx @@ -15,12 +15,14 @@ import { FormEmail, type FormValues } from "@/components/forms/form-email"; import { generateServerActionPromise } from "@/lib/server-actions"; import { Button } from "@openstatus/ui/components/ui/button"; import { Inbox } from "lucide-react"; +import { useExtracted } from "next-intl"; import { useParams } from "next/navigation"; import { useState } from "react"; import { flushSync } from "react-dom"; import { signInWithResendAction } from "../actions"; export function SectionMagicLink() { + const t = useExtracted(); const { domain } = useParams<{ domain: string }>(); const [state, setState] = useState<"idle" | "pending" | "success">("idle"); @@ -54,10 +56,9 @@ export function SectionMagicLink() { return (
    - Authenticate + {t("Authenticate")} - Enter your email to receive a magic link for accessing the status - page. Note: Only emails from approved domains are accepted. + {t("Enter your email to receive a magic link for accessing the status page. Note: Only emails from approved domains are accepted.")} {state !== "success" ? ( @@ -68,7 +69,7 @@ export function SectionMagicLink() { form="email-form" disabled={state === "pending"} > - {state === "pending" ? "Submitting..." : "Submit"} + {state === "pending" ? t("Submitting...") : t("Submit")}
) : ( @@ -79,12 +80,13 @@ export function SectionMagicLink() { } function SuccessState() { + const t = useExtracted(); return ( - Check your inbox! + {t("Check your inbox!")} - Access the status page by clicking the link in the email. + {t("Access the status page by clicking the link in the email.")} ); diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-password.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-password.tsx index e42c6ce547..69b166c7e6 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-password.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-password.tsx @@ -12,9 +12,11 @@ import { useTRPC } from "@/lib/trpc/client"; import { Button } from "@openstatus/ui/components/ui/button"; import { useCookieState } from "@openstatus/ui/hooks/use-cookie-state"; import { useMutation } from "@tanstack/react-query"; +import { useExtracted } from "next-intl"; import { useParams, useRouter, useSearchParams } from "next/navigation"; export function SectionPassword() { + const t = useExtracted(); const { domain } = useParams<{ domain: string }>(); const searchParams = useSearchParams(); const trpc = useTRPC(); @@ -27,9 +29,9 @@ export function SectionPassword() { return (
- Protected Page + {t("Protected Page")} - Enter the password to access the status page. + {t("Enter the password to access the status page.")}
@@ -48,7 +50,7 @@ export function SectionPassword() { }} />
diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/actions.ts b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/actions.ts index 57c1e32c28..80834c1d1a 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/actions.ts +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/actions.ts @@ -4,9 +4,11 @@ import { signIn } from "@/lib/auth"; import { getQueryClient, trpc } from "@/lib/trpc/server"; import { TRPCClientError } from "@trpc/client"; import { AuthError } from "next-auth"; +import { getExtracted } from "next-intl/server"; import { isRedirectError } from "next/dist/client/components/redirect-error"; export async function signInWithResendAction(formData: FormData) { + const t = await getExtracted(); try { const email = formData.get("email") as string; const redirectTo = formData.get("redirectTo") as string; @@ -15,7 +17,7 @@ export async function signInWithResendAction(formData: FormData) { if (!email || !redirectTo) { return { success: false, - error: "Email and redirectTo are required", + error: t("Email and redirectTo are required"), }; } @@ -38,7 +40,7 @@ export async function signInWithResendAction(formData: FormData) { } return { success: false, - error: "An unexpected error occurred during sign in", + error: t("An unexpected error occurred during sign in"), }; } diff --git a/apps/status-page/src/components/forms/form-email.tsx b/apps/status-page/src/components/forms/form-email.tsx index 31094caab5..4d7f4b78f6 100644 --- a/apps/status-page/src/components/forms/form-email.tsx +++ b/apps/status-page/src/components/forms/form-email.tsx @@ -10,6 +10,7 @@ import { } from "@openstatus/ui/components/ui/form"; import { Input } from "@openstatus/ui/components/ui/input"; import { isTRPCClientError } from "@trpc/client"; +import { useExtracted } from "next-intl"; import { useTransition } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -27,6 +28,7 @@ export function FormEmail({ }: Omit, "onSubmit"> & { onSubmit: (values: FormValues) => Promise; }) { + const t = useExtracted(); const form = useForm({ resolver: zodResolver(schema), defaultValues: { @@ -42,8 +44,8 @@ export function FormEmail({ try { const promise = onSubmit(values); toast.promise(promise, { - loading: "Confirming...", - success: "Confirmed", + loading: t("Confirming..."), + success: t("Confirmed"), error: (error) => { console.error(error); if (isTRPCClientError(error)) { @@ -54,7 +56,7 @@ export function FormEmail({ form.setError("email", { message: error.message }); return error.message; } - return "Failed to confirm"; + return t("Failed to confirm"); }, }); await promise; @@ -72,7 +74,7 @@ export function FormEmail({ name="email" render={({ field }) => ( - Email + {t("Email")} diff --git a/apps/status-page/src/components/forms/form-manage-subscription.tsx b/apps/status-page/src/components/forms/form-manage-subscription.tsx index fc2e41b6cc..1852545391 100644 --- a/apps/status-page/src/components/forms/form-manage-subscription.tsx +++ b/apps/status-page/src/components/forms/form-manage-subscription.tsx @@ -18,6 +18,7 @@ import { import { Separator } from "@openstatus/ui/components/ui/separator"; import { cn } from "@openstatus/ui/lib/utils"; import { isTRPCClientError } from "@trpc/client"; +import { useExtracted } from "next-intl"; import { useTransition } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -44,6 +45,7 @@ export function FormManageSubscription({ page?: Page | null; defaultValues?: FormValues; }) { + const t = useExtracted(); const form = useForm({ resolver: zodResolver(schema), defaultValues: { @@ -60,13 +62,13 @@ export function FormManageSubscription({ try { const promise = onSubmit(values); toast.promise(promise, { - loading: "Updating subscription...", - success: "Subscription updated", + loading: t("Updating subscription..."), + success: t("Subscription updated"), error: (error) => { if (isTRPCClientError(error)) { return error.message; } - return "Failed to update subscription"; + return t("Failed to update subscription"); }, }); await promise; @@ -96,7 +98,7 @@ export function FormManageSubscription({ }} /> - Subscribe to specific components + {t("Subscribe to specific components")} )} /> @@ -224,10 +226,10 @@ export function FormManageSubscription({ ) : ( - No components to subscribe to + {t("No components to subscribe to")} - This status page has no components to subscribe to. + {t("This status page has no components to subscribe to.")} )} diff --git a/apps/status-page/src/components/forms/form-password.tsx b/apps/status-page/src/components/forms/form-password.tsx index 9d373481c8..66dbd65277 100644 --- a/apps/status-page/src/components/forms/form-password.tsx +++ b/apps/status-page/src/components/forms/form-password.tsx @@ -10,6 +10,7 @@ import { } from "@openstatus/ui/components/ui/form"; import { Input } from "@openstatus/ui/components/ui/input"; import { isTRPCClientError } from "@trpc/client"; +import { useExtracted } from "next-intl"; import { useTransition } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -27,6 +28,7 @@ export function FormPassword({ }: Omit, "onSubmit"> & { onSubmit: (values: FormValues) => Promise; }) { + const t = useExtracted(); const form = useForm({ resolver: zodResolver(schema), defaultValues: { @@ -42,14 +44,14 @@ export function FormPassword({ try { const promise = onSubmit(values); toast.promise(promise, { - loading: "Confirming...", - success: "Confirmed", + loading: t("Confirming..."), + success: t("Confirmed"), error: (error) => { if (isTRPCClientError(error)) { form.setError("password", { message: error.message }); return error.message; } - return "Failed to confirm"; + return t("Failed to confirm"); }, }); await promise; @@ -67,7 +69,7 @@ export function FormPassword({ name="password" render={({ field }) => ( - Password + {t("Password")} diff --git a/apps/status-page/src/components/forms/form-subscribe-email.tsx b/apps/status-page/src/components/forms/form-subscribe-email.tsx index 0c78669d9b..02e8c7ba75 100644 --- a/apps/status-page/src/components/forms/form-subscribe-email.tsx +++ b/apps/status-page/src/components/forms/form-subscribe-email.tsx @@ -19,6 +19,7 @@ import { Input } from "@openstatus/ui/components/ui/input"; import { Separator } from "@openstatus/ui/components/ui/separator"; import { cn } from "@openstatus/ui/lib/utils"; import { isTRPCClientError } from "@trpc/client"; +import { useExtracted } from "next-intl"; import { useTransition } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -44,6 +45,7 @@ export function FormSubscribeEmail({ onSubmitCallback?: () => void; page?: Page | null; }) { + const t = useExtracted(); const form = useForm({ resolver: zodResolver(schema), defaultValues: { @@ -61,13 +63,13 @@ export function FormSubscribeEmail({ try { const promise = onSubmit(values); toast.promise(promise, { - loading: "Subscribing...", - success: "Subscribed", + loading: t("Subscribing..."), + success: t("Subscribed"), error: (error) => { if (isTRPCClientError(error)) { return error.message; } - return "Failed to subscribe"; + return t("Failed to subscribe"); }, }); await promise; @@ -89,7 +91,7 @@ export function FormSubscribeEmail({ name="email" render={({ field }) => ( - Email + {t("Email")} @@ -108,7 +110,7 @@ export function FormSubscribeEmail({ onCheckedChange={field.onChange} /> - Subscribe to specific components + {t("Subscribe to specific components")} )} /> @@ -242,10 +244,10 @@ export function FormSubscribeEmail({ ) : ( - No components to subscribe to + {t("No components to subscribe to")} - This page has no components to subscribe to. + {t("This page has no components to subscribe to.")} )} diff --git a/apps/status-page/src/components/status-page/status-updates.tsx b/apps/status-page/src/components/status-page/status-updates.tsx index 67b59ab65e..2d997dbeb8 100644 --- a/apps/status-page/src/components/status-page/status-updates.tsx +++ b/apps/status-page/src/components/status-page/status-updates.tsx @@ -23,6 +23,7 @@ import { import { useCopyToClipboard } from "@openstatus/ui/hooks/use-copy-to-clipboard"; import { cn } from "@openstatus/ui/lib/utils"; import { Check, Copy, Inbox } from "lucide-react"; +import { useExtracted } from "next-intl"; import { useState } from "react"; export type StatusUpdateType = "email" | "rss" | "ssh" | "json" | "slack"; @@ -55,6 +56,7 @@ export function StatusUpdates({ onSubscribe, ...props }: StatusUpdatesProps) { + const t = useExtracted(); const [success, setSuccess] = useState(false); if (types.length === 0) return null; @@ -68,26 +70,26 @@ export function StatusUpdates({ className={cn(className)} {...props} > - Get updates + {t("Get updates")} {types.includes("email") ? ( - Email + {t("Email")} ) : null} {types.includes("slack") ? ( - Slack + {t("Slack")} ) : null} {types.includes("rss") ? ( - RSS + {t("RSS")} ) : null} {types.includes("json") ? ( - JSON + {t("JSON")} ) : null} {types.includes("ssh") ? ( - SSH + {t("SSH")} ) : null} @@ -97,8 +99,7 @@ export function StatusUpdates({ <>

- Get email notifications whenever a report has been created - or resolved + {t("Get email notifications whenever a report has been created or resolved")}

{" "} @@ -120,7 +121,7 @@ export function StatusUpdates({
-

Get the RSS feed

+

{t("Get the RSS feed")}

-

Get the Atom feed

+

{t("Get the Atom feed")}

-

Get the JSON updates

+

{t("Get the JSON updates")}

-

Get status via SSH

+

{t("Get status via SSH")}

- For status updates in Slack, paste the text below into any - channel. + {t("For status updates in Slack, paste the text below into any channel.")}

& { value: string; }) { + const t = useExtracted(); const { copy, isCopied } = useCopyToClipboard(); return (
@@ -191,7 +192,7 @@ function CopyInputButton({ readOnly onClick={(e) => { copy(value, { - successMessage: "Link copied to clipboard", + successMessage: t("Link copied to clipboard"), withToast: true, }); onClick?.(e); @@ -203,25 +204,26 @@ function CopyInputButton({ size="icon" onClick={() => copy(value, { - successMessage: "Link copied to clipboard", + successMessage: t("Link copied to clipboard"), }) } className="-translate-y-1/2 absolute top-1/2 right-2 size-6" > {isCopied ? : } - Copy Link + {t("Copy Link")}
); } function SuccessMessage() { + const t = useExtracted(); return (
-

Check your inbox!

+

{t("Check your inbox!")}

- Validate your email to receive updates and you are all set. + {t("Validate your email to receive updates and you are all set.")}

); From 7a5240b4a0244c1190b3b114cc4df0ee4a2e1d1b Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Sat, 7 Mar 2026 18:56:48 +0530 Subject: [PATCH 08/24] feat(status-page): delete old messages.ts, add language switcher Remove the old messages.ts file (replaced by useExtracted), update status-tracker.tsx to use useExtracted, and add a globe-icon language switcher dropdown in the footer. Co-Authored-By: Claude Opus 4.6 --- .../src/components/language-switcher.tsx | 82 +++++++++++++++++++ .../status-page/src/components/nav/footer.tsx | 2 + .../src/components/status-page/messages.ts | 31 ------- .../components/status-page/status-tracker.tsx | 15 +++- 4 files changed, 96 insertions(+), 34 deletions(-) create mode 100644 apps/status-page/src/components/language-switcher.tsx delete mode 100644 apps/status-page/src/components/status-page/messages.ts diff --git a/apps/status-page/src/components/language-switcher.tsx b/apps/status-page/src/components/language-switcher.tsx new file mode 100644 index 0000000000..25bc8acc37 --- /dev/null +++ b/apps/status-page/src/components/language-switcher.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { locales } from "@/i18n/config"; +import { Button } from "@openstatus/ui/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@openstatus/ui/components/ui/dropdown-menu"; +import { GlobeIcon } from "lucide-react"; +import { useLocale } from "next-intl"; +import { useParams, usePathname, useRouter } from "next/navigation"; +import { useTransition } from "react"; + +const localeNames: Record = { + en: "English", + es: "Español", + fr: "Français", + de: "Deutsch", + pt: "Português", + ja: "日本語", + zh: "中文", + ko: "한국어", +}; + +export function LanguageSwitcher() { + const locale = useLocale(); + const router = useRouter(); + const pathname = usePathname(); + const params = useParams(); + const [isPending, startTransition] = useTransition(); + + function onSelectLocale(nextLocale: string) { + startTransition(() => { + const segments = pathname.split("/"); + const domain = params.domain as string; + const domainIndex = segments.indexOf(domain); + + const potentialLocale = segments[domainIndex + 1]; + const hasLocaleSegment = locales.includes(potentialLocale as any); + + if (nextLocale === "en") { + if (hasLocaleSegment) { + segments.splice(domainIndex + 1, 1); + } + } else if (hasLocaleSegment) { + segments[domainIndex + 1] = nextLocale; + } else { + segments.splice(domainIndex + 1, 0, nextLocale); + } + + router.replace(segments.join("/") || "/"); + }); + } + + return ( + + + + + + {locales.map((loc) => ( + onSelectLocale(loc)} + className={loc === locale ? "font-bold" : ""} + > + {localeNames[loc]} + + ))} + + + ); +} diff --git a/apps/status-page/src/components/nav/footer.tsx b/apps/status-page/src/components/nav/footer.tsx index a0b5c1e42d..0370ab2219 100644 --- a/apps/status-page/src/components/nav/footer.tsx +++ b/apps/status-page/src/components/nav/footer.tsx @@ -2,6 +2,7 @@ import { Link } from "@/components/common/link"; import { TimestampHoverCard } from "@/components/content/timestamp-hover-card"; +import { LanguageSwitcher } from "@/components/language-switcher"; import { ThemeDropdown } from "@/components/themes/theme-dropdown"; import { useTRPC } from "@/lib/trpc/client"; import { Skeleton } from "@openstatus/ui/components/ui/skeleton"; @@ -60,6 +61,7 @@ export function Footer(props: React.ComponentProps<"footer">) { )} +
diff --git a/apps/status-page/src/components/status-page/messages.ts b/apps/status-page/src/components/status-page/messages.ts deleted file mode 100644 index 071a4bd530..0000000000 --- a/apps/status-page/src/components/status-page/messages.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const messages = { - long: { - success: "All Systems Operational", - degraded: "Degraded Performance", - error: "Downtime Performance", - info: "Maintenance", - empty: "No Data", - }, - short: { - success: "Operational", - degraded: "Degraded", - error: "Downtime", - info: "Maintenance", - empty: "No Data", - }, -}; - -export const requests = { - success: "Normal", - degraded: "Degraded", - error: "Error", - info: "Maintenance", - empty: "No Data", -}; - -export const status = { - resolved: "Resolved", - monitoring: "Monitoring", - identified: "Identified", - investigating: "Investigating", -}; diff --git a/apps/status-page/src/components/status-page/status-tracker.tsx b/apps/status-page/src/components/status-page/status-tracker.tsx index 01883b9ab9..1a6748319a 100644 --- a/apps/status-page/src/components/status-page/status-tracker.tsx +++ b/apps/status-page/src/components/status-page/status-tracker.tsx @@ -15,8 +15,8 @@ import { useMediaQuery } from "@openstatus/ui/hooks/use-media-query"; import { cn } from "@openstatus/ui/lib/utils"; import { formatDistanceStrict } from "date-fns"; import Link from "next/link"; +import { useExtracted } from "next-intl"; import { useEffect, useRef, useState } from "react"; -import { requests } from "./messages"; import { chartConfig } from "./utils"; type UptimeData = NonNullable< @@ -32,6 +32,7 @@ type UptimeData = NonNullable< // TODO: widget type -> current status only | with status history export function StatusTracker({ data }: { data: UptimeData }) { + const t = useExtracted(); const [pinnedIndex, setPinnedIndex] = useState(null); const [focusedIndex, setFocusedIndex] = useState(null); const [hoveredIndex, setHoveredIndex] = useState(null); @@ -342,7 +343,7 @@ export function StatusTracker({ data }: { data: UptimeData }) { <>
- Click again to unpin + {t("Click again to unpin")} Esc
@@ -375,6 +376,14 @@ function StatusTrackerContent({ status: "success" | "degraded" | "error" | "info" | "empty"; value: string; }) { + const t = useExtracted(); + const requestLabels: Record = { + success: t("Normal"), + degraded: t("Degraded"), + error: t("Error"), + info: t("Maintenance"), + empty: t("No Data"), + }; return (
@@ -384,7 +393,7 @@ function StatusTrackerContent({ backgroundColor: chartConfig[status].color, }} /> -
{requests[status]}
+
{requestLabels[status]}
{value} From b5f131b7a87763824169d83ec6747baa3d65224e Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Sat, 7 Mar 2026 18:57:31 +0530 Subject: [PATCH 09/24] feat(status-page): make pathname prefix locale-aware Update usePathnamePrefix hook to include locale segment in prefix for non-default locales, ensuring internal links work correctly across all languages. Co-Authored-By: Claude Opus 4.6 --- .../src/hooks/use-pathname-prefix.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/status-page/src/hooks/use-pathname-prefix.ts b/apps/status-page/src/hooks/use-pathname-prefix.ts index 065fcd03bc..acb0497dc0 100644 --- a/apps/status-page/src/hooks/use-pathname-prefix.ts +++ b/apps/status-page/src/hooks/use-pathname-prefix.ts @@ -1,7 +1,9 @@ "use client"; +import { defaultLocale } from "@/i18n/config"; import { useTRPC } from "@/lib/trpc/client"; import { useQuery } from "@tanstack/react-query"; +import { useLocale } from "next-intl"; import { useParams } from "next/navigation"; import { useEffect, useState } from "react"; @@ -11,12 +13,12 @@ export function usePathnamePrefix() { const { data: page } = useQuery({ ...trpc.statusPage.get.queryOptions({ slug: domain }), }); + const locale = useLocale(); const [prefix, setPrefix] = useState(""); useEffect(() => { if (typeof window !== "undefined") { const hostnames = window.location.hostname.split("."); - const pathnames = window.location.pathname.split("/"); const isCustomDomain = window.location.hostname === page?.customDomain; if ( @@ -25,12 +27,21 @@ export function usePathnamePrefix() { hostnames[0] !== "www" && !window.location.hostname.endsWith(".vercel.app")) ) { - setPrefix(""); + // Subdomain or custom domain — no domain prefix needed + // But locale prefix is needed for non-default locale + setPrefix(locale !== defaultLocale ? locale : ""); } else { - setPrefix(pathnames[1] || ""); + const pathnames = window.location.pathname.split("/"); + const domainSegment = pathnames[1] || ""; + // Include locale in prefix for non-default locale + if (locale !== defaultLocale) { + setPrefix(`${domainSegment}/${locale}`); + } else { + setPrefix(domainSegment); + } } } - }, [page?.customDomain]); + }, [page?.customDomain, locale]); return prefix; } From 2e36a0d828a618526ffbfe875001d00dd3b3f890 Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Sat, 7 Mar 2026 19:13:50 +0530 Subject: [PATCH 10/24] fix(status-page): switch message catalogs from .po to .json format Turbopack doesn't support .po imports natively. Switch to .json which next-intl handles out of the box. Create empty JSON catalogs for all supported locales as placeholders for future translations. Co-Authored-By: Claude Opus 4.6 --- apps/status-page/messages/de.json | 1 + apps/status-page/messages/en.json | 1 + apps/status-page/messages/en.po | 2 -- apps/status-page/messages/es.json | 1 + apps/status-page/messages/fr.json | 1 + apps/status-page/messages/ja.json | 1 + apps/status-page/messages/ko.json | 1 + apps/status-page/messages/pt.json | 1 + apps/status-page/messages/zh.json | 1 + .../src/app/(status-page)/[domain]/[locale]/layout.tsx | 2 +- apps/status-page/src/i18n/request.ts | 2 +- 11 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 apps/status-page/messages/de.json create mode 100644 apps/status-page/messages/en.json delete mode 100644 apps/status-page/messages/en.po create mode 100644 apps/status-page/messages/es.json create mode 100644 apps/status-page/messages/fr.json create mode 100644 apps/status-page/messages/ja.json create mode 100644 apps/status-page/messages/ko.json create mode 100644 apps/status-page/messages/pt.json create mode 100644 apps/status-page/messages/zh.json diff --git a/apps/status-page/messages/de.json b/apps/status-page/messages/de.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/apps/status-page/messages/de.json @@ -0,0 +1 @@ +{} diff --git a/apps/status-page/messages/en.json b/apps/status-page/messages/en.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/apps/status-page/messages/en.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/apps/status-page/messages/en.po b/apps/status-page/messages/en.po deleted file mode 100644 index 8c1ebce251..0000000000 --- a/apps/status-page/messages/en.po +++ /dev/null @@ -1,2 +0,0 @@ -# English translations (auto-extracted by next-intl) -# This file is auto-generated. Do not edit manually. diff --git a/apps/status-page/messages/es.json b/apps/status-page/messages/es.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/apps/status-page/messages/es.json @@ -0,0 +1 @@ +{} diff --git a/apps/status-page/messages/fr.json b/apps/status-page/messages/fr.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/apps/status-page/messages/fr.json @@ -0,0 +1 @@ +{} diff --git a/apps/status-page/messages/ja.json b/apps/status-page/messages/ja.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/apps/status-page/messages/ja.json @@ -0,0 +1 @@ +{} diff --git a/apps/status-page/messages/ko.json b/apps/status-page/messages/ko.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/apps/status-page/messages/ko.json @@ -0,0 +1 @@ +{} diff --git a/apps/status-page/messages/pt.json b/apps/status-page/messages/pt.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/apps/status-page/messages/pt.json @@ -0,0 +1 @@ +{} diff --git a/apps/status-page/messages/zh.json b/apps/status-page/messages/zh.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/apps/status-page/messages/zh.json @@ -0,0 +1 @@ +{} diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx index 2976c9a708..c91921c7ff 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx @@ -18,7 +18,7 @@ export default async function LocaleLayout({ setRequestLocale(locale); - const messages = (await import(`@/../../messages/${locale}.po`)).default; + const messages = (await import(`../../../../../messages/${locale}.json`)).default; return ( diff --git a/apps/status-page/src/i18n/request.ts b/apps/status-page/src/i18n/request.ts index 54f8e68604..3d4ad8b315 100644 --- a/apps/status-page/src/i18n/request.ts +++ b/apps/status-page/src/i18n/request.ts @@ -10,6 +10,6 @@ export default getRequestConfig(async ({ requestLocale }) => { return { locale, - messages: (await import(`../../messages/${locale}.po`)).default, + messages: (await import(`../../messages/${locale}.json`)).default, }; }); From 5a4e38176e5ac297166e1775bee9e255cb8fae29 Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Sat, 7 Mar 2026 21:21:51 +0530 Subject: [PATCH 11/24] Format fix Signed-off-by: Moulik Aggarwal --- apps/status-page/messages/en.json | 2 +- apps/status-page/next.config.ts | 2 +- .../(auth)/login/_components/section-magic-link.tsx | 4 +++- .../[domain]/[locale]/(public)/monitors/[id]/page.tsx | 9 ++++++--- .../app/(status-page)/[domain]/[locale]/layout.tsx | 5 +++-- .../src/components/status-page/status-tracker.tsx | 2 +- .../src/components/status-page/status-updates.tsx | 8 ++++++-- apps/status-page/src/i18n/config.ts | 11 ++++++++++- apps/status-page/src/i18n/routing.ts | 2 +- 9 files changed, 32 insertions(+), 13 deletions(-) diff --git a/apps/status-page/messages/en.json b/apps/status-page/messages/en.json index 9e26dfeeb6..0967ef424b 100644 --- a/apps/status-page/messages/en.json +++ b/apps/status-page/messages/en.json @@ -1 +1 @@ -{} \ No newline at end of file +{} diff --git a/apps/status-page/next.config.ts b/apps/status-page/next.config.ts index d2aaa45877..f66ef37bdf 100644 --- a/apps/status-page/next.config.ts +++ b/apps/status-page/next.config.ts @@ -1,6 +1,6 @@ import { withSentryConfig } from "@sentry/nextjs"; -import createNextIntlPlugin from "next-intl/plugin"; import type { NextConfig } from "next"; +import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-magic-link.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-magic-link.tsx index 23b8c30a00..6064b32d6c 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-magic-link.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-magic-link.tsx @@ -58,7 +58,9 @@ export function SectionMagicLink() { {t("Authenticate")} - {t("Enter your email to receive a magic link for accessing the status page. Note: Only emails from approved domains are accepted.")} + {t( + "Enter your email to receive a magic link for accessing the status page. Note: Only emails from approved domains are accepted.", + )} {state !== "success" ? ( diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx index 51bb06be9a..6e6fe4c351 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(public)/monitors/[id]/page.tsx @@ -14,7 +14,6 @@ import { ChartLineRegions, ChartLineRegionsSkeleton, } from "@/components/chart/chart-line-regions"; -import { PopoverQuantile } from "@/components/popover/popover-quantile"; import { Status, StatusContent, @@ -285,7 +284,9 @@ export default function Page() { {t("Global Latency")} - {t("The aggregated latency from all active regions based on different quantiles.")} + {t( + "The aggregated latency from all active regions based on different quantiles.", + )} {isLoading ? ( @@ -307,7 +308,9 @@ export default function Page() { {t("Latency by Region")} - {t("Region latency per p75 quantile, sorted by slowest region. Compare up to 6 regions.")} + {t( + "Region latency per p75 quantile, sorted by slowest region. Compare up to 6 regions.", + )} {isLoading ? ( diff --git a/apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx index c91921c7ff..4e0f39fffb 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx @@ -1,7 +1,7 @@ +import { routing } from "@/i18n/routing"; import { NextIntlClientProvider, hasLocale } from "next-intl"; import { setRequestLocale } from "next-intl/server"; import { notFound } from "next/navigation"; -import { routing } from "@/i18n/routing"; export default async function LocaleLayout({ children, @@ -18,7 +18,8 @@ export default async function LocaleLayout({ setRequestLocale(locale); - const messages = (await import(`../../../../../messages/${locale}.json`)).default; + const messages = (await import(`../../../../../messages/${locale}.json`)) + .default; return ( diff --git a/apps/status-page/src/components/status-page/status-tracker.tsx b/apps/status-page/src/components/status-page/status-tracker.tsx index 1a6748319a..7005c03d66 100644 --- a/apps/status-page/src/components/status-page/status-tracker.tsx +++ b/apps/status-page/src/components/status-page/status-tracker.tsx @@ -14,8 +14,8 @@ import { Skeleton } from "@openstatus/ui/components/ui/skeleton"; import { useMediaQuery } from "@openstatus/ui/hooks/use-media-query"; import { cn } from "@openstatus/ui/lib/utils"; import { formatDistanceStrict } from "date-fns"; -import Link from "next/link"; import { useExtracted } from "next-intl"; +import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { chartConfig } from "./utils"; diff --git a/apps/status-page/src/components/status-page/status-updates.tsx b/apps/status-page/src/components/status-page/status-updates.tsx index 2d997dbeb8..225d292a11 100644 --- a/apps/status-page/src/components/status-page/status-updates.tsx +++ b/apps/status-page/src/components/status-page/status-updates.tsx @@ -99,7 +99,9 @@ export function StatusUpdates({ <>

- {t("Get email notifications whenever a report has been created or resolved")} + {t( + "Get email notifications whenever a report has been created or resolved", + )}

- {t("For status updates in Slack, paste the text below into any channel.")} + {t( + "For status updates in Slack, paste the text below into any channel.", + )}

Date: Thu, 12 Mar 2026 21:37:04 +0530 Subject: [PATCH 12/24] WIP Status Page static and domain specific Signed-off-by: Moulik Aggarwal --- apps/status-page/next.config.ts | 31 +- apps/status-page/src/app/(public)/client.tsx | 7 +- .../src/components/language-switcher.tsx | 2 +- .../static/status-banner-static.tsx | 48 ++ .../static/status-events-static.tsx | 120 +++++ .../static/status-monitor-static.tsx | 167 +++++++ .../static/status-tracker-static.tsx | 415 ++++++++++++++++++ .../components/status-page/status-events.tsx | 3 - apps/status-page/src/i18n/request.ts | 24 +- 9 files changed, 807 insertions(+), 10 deletions(-) create mode 100644 apps/status-page/src/components/status-page/static/status-banner-static.tsx create mode 100644 apps/status-page/src/components/status-page/static/status-events-static.tsx create mode 100644 apps/status-page/src/components/status-page/static/status-monitor-static.tsx create mode 100644 apps/status-page/src/components/status-page/static/status-tracker-static.tsx diff --git a/apps/status-page/next.config.ts b/apps/status-page/next.config.ts index f66ef37bdf..d2871e8013 100644 --- a/apps/status-page/next.config.ts +++ b/apps/status-page/next.config.ts @@ -23,6 +23,35 @@ const nextConfig: NextConfig = { async rewrites() { return { beforeFiles: [ + // When URL already has a locale prefix (e.g. /fr/events → /subdomain/fr/events) + { + source: "/:locale(en|es|fr|de|pt|ja|zh|ko)/:path*", + has: [ + { + type: "host", + value: + process.env.NODE_ENV === "production" + ? "(?[^.]+).stpg.dev" + : "(?[^.]+).localhost", + }, + ], + missing: [ + { + type: "header", + key: "x-proxy", + value: "1", + }, + { + type: "host", + value: + process.env.NODE_ENV === "production" + ? "www.stpg.dev" + : "localhost", + }, + ], + destination: "/:subdomain/:locale/:path*", + }, + // When URL has no locale prefix (e.g. /events → /subdomain/en/events) { source: "/:path((?!api|assets|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)", @@ -50,7 +79,7 @@ const nextConfig: NextConfig = { : "localhost", }, ], - destination: "/:subdomain/:path*", + destination: "/:subdomain/en/:path*", }, ], }; diff --git a/apps/status-page/src/app/(public)/client.tsx b/apps/status-page/src/app/(public)/client.tsx index a11495dc46..ba5eafd9b4 100644 --- a/apps/status-page/src/app/(public)/client.tsx +++ b/apps/status-page/src/app/(public)/client.tsx @@ -17,8 +17,7 @@ import { StatusHeader, StatusTitle, } from "@/components/status-page/status"; -import { StatusBanner } from "@/components/status-page/status-banner"; -import { StatusMonitor } from "@/components/status-page/status-monitor"; +import { StatusBannerStatic } from "@/components/status-page/static/status-banner-static"; import { ThemePalettePicker } from "@/components/themes/theme-palette-picker"; import { ThemeSelect } from "@/components/themes/theme-select"; import { monitors } from "@/data/monitors"; @@ -295,10 +294,10 @@ function ThemePlaygroundStatus({ Get informed about our services. - + {/* TODO: create mock data */} - & { + status?: "success" | "degraded" | "error" | "info"; +}) { + return ( + + +
+
+ + All Systems Operational + + + Degraded Performance + + + Downtime Performance + + + Maintenance + +
+ +
+
+ ); +} diff --git a/apps/status-page/src/components/status-page/static/status-events-static.tsx b/apps/status-page/src/components/status-page/static/status-events-static.tsx new file mode 100644 index 0000000000..034d6376f5 --- /dev/null +++ b/apps/status-page/src/components/status-page/static/status-events-static.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { ProcessMessage } from "@/components/content/process-message"; +import { TimestampHoverCard } from "@/components/content/timestamp-hover-card"; +import { formatDateTime } from "@/lib/formatter"; +import { cn } from "@openstatus/ui/lib/utils"; +import { formatDistanceStrict } from "date-fns"; +import { + StatusEventTimelineDot, + StatusEventTimelineMessage, + StatusEventTimelineSeparator, + StatusEventTimelineTitle, +} from "../status-events"; + +export function StatusEventTimelineReportStatic({ + className, + updates, + withDot = true, + maxUpdates, + ...props +}: React.ComponentProps<"div"> & { + reportId: number; + updates: { + date: Date; + message: string; + status: "investigating" | "identified" | "monitoring" | "resolved"; + }[]; + withDot?: boolean; + maxUpdates?: number; +}) { + const sortedUpdates = [...updates].sort( + (a, b) => b.date.getTime() - a.date.getTime(), + ); + const displayedUpdates = maxUpdates + ? sortedUpdates.slice(0, maxUpdates) + : sortedUpdates; + + const statusLabels = { + resolved: "Resolved", + monitoring: "Monitoring", + identified: "Identified", + investigating: "Investigating", + } as const; + + return ( +
+ {displayedUpdates.map((update, index) => { + const updateDate = new Date(update.date); + let durationText: string | undefined; + + if (index === 0) { + const startedAt = new Date( + sortedUpdates[sortedUpdates.length - 1].date, + ); + const duration = formatDistanceStrict(startedAt, updateDate); + + if (duration !== "0 seconds" && update.status === "resolved") { + durationText = `(in ${duration})`; + } + } else { + const lastUpdateDate = new Date(displayedUpdates[index - 1].date); + const timeFromLast = formatDistanceStrict( + updateDate, + lastUpdateDate, + ); + durationText = `(${timeFromLast} earlier)`; + } + + return ( +
+
+
+ {withDot ? ( +
+
+ +
+ {index !== displayedUpdates.length - 1 ? ( + + ) : null} +
+ ) : null} +
+ + {statusLabels[update.status]}{" "} + ·{" "} + + + {formatDateTime(update.date)} + + {" "} + {durationText ? ( + + {durationText} + + ) : null} + + + {update.message.trim() === "" ? ( + - + ) : ( + + )} + +
+
+
+
+ ); + })} +
+ ); +} diff --git a/apps/status-page/src/components/status-page/static/status-monitor-static.tsx b/apps/status-page/src/components/status-page/static/status-monitor-static.tsx new file mode 100644 index 0000000000..9e5f8a4a0e --- /dev/null +++ b/apps/status-page/src/components/status-page/static/status-monitor-static.tsx @@ -0,0 +1,167 @@ +"use client"; + +import type { RouterOutputs } from "@openstatus/api"; +import { Skeleton } from "@openstatus/ui/components/ui/skeleton"; +import { cn } from "@openstatus/ui/lib/utils"; +import { formatDistanceToNowStrict } from "date-fns"; +import { + AlertCircleIcon, + CheckIcon, + TriangleAlertIcon, + WrenchIcon, +} from "lucide-react"; +import { + StatusMonitorDescription, + StatusMonitorTitle, + StatusMonitorUptime, + StatusMonitorUptimeSkeleton, +} from "../status-monitor"; +import { StatusTrackerSkeleton } from "../status-tracker"; +import { StatusTrackerStatic } from "./status-tracker-static"; + +type VariantType = "success" | "degraded" | "error" | "info"; + +type Data = NonNullable< + RouterOutputs["statusPage"]["getUptime"] +>[number]["data"]; + +export function StatusMonitorStatic({ + className, + status = "success", + showUptime = true, + data = [], + monitor, + uptime, + isLoading = false, + ...props +}: React.ComponentProps<"div"> & { + status?: VariantType; + showUptime?: boolean; + uptime?: string; + monitor: { + name: string; + description?: string | null; + }; + data?: Data; + isLoading?: boolean; +}) { + return ( +
+
+
+ {monitor.name} + + {monitor.description} + +
+
+ {showUptime ? ( + <> + {isLoading ? ( + + ) : ( + {uptime} + )} + + + ) : ( + + )} +
+
+ {isLoading ? ( + + ) : ( + + )} + +
+ ); +} + +function StatusMonitorFooterStatic({ + data, + isLoading, +}: { + data: Data; + isLoading?: boolean; +}) { + return ( +
+
+ {isLoading ? ( + + ) : data.length > 0 ? ( + formatDistanceToNowStrict(new Date(data[0].day), { + unit: "day", + addSuffix: true, + }) + ) : ( + "-" + )} +
+
today
+
+ ); +} + +function StatusMonitorIconStatic({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
svg]:size-[9px]", + "group-data-[variant=success]/monitor:bg-success", + "group-data-[variant=degraded]/monitor:bg-warning", + "group-data-[variant=error]/monitor:bg-destructive", + "group-data-[variant=info]/monitor:bg-info", + className, + )} + {...props} + > + + + + +
+ ); +} + +function StatusMonitorStatusStatic({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ + Operational + + + Degraded + + + Downtime + + + Maintenance + +
+ ); +} diff --git a/apps/status-page/src/components/status-page/static/status-tracker-static.tsx b/apps/status-page/src/components/status-page/static/status-tracker-static.tsx new file mode 100644 index 0000000000..6ea54a0c48 --- /dev/null +++ b/apps/status-page/src/components/status-page/static/status-tracker-static.tsx @@ -0,0 +1,415 @@ +"use client"; + +import { Kbd } from "@/components/common/kbd"; +import { formatDateRange } from "@/lib/formatter"; +import type { RouterOutputs } from "@openstatus/api"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@openstatus/ui/components/ui/hover-card"; +import { Separator } from "@openstatus/ui/components/ui/separator"; +import { useMediaQuery } from "@openstatus/ui/hooks/use-media-query"; +import { cn } from "@openstatus/ui/lib/utils"; +import { formatDistanceStrict } from "date-fns"; +import { useEffect, useRef, useState } from "react"; +import { chartConfig } from "../utils"; + +type UptimeData = NonNullable< + RouterOutputs["statusPage"]["getUptime"] +>[number]["data"]; + +const staticLabels: Record = { + success: "Normal", + degraded: "Degraded", + error: "Error", + info: "Maintenance", + empty: "No Data", +}; + +export function StatusTrackerStatic({ data }: { data: UptimeData }) { + const [pinnedIndex, setPinnedIndex] = useState(null); + const [focusedIndex, setFocusedIndex] = useState(null); + const [hoveredIndex, setHoveredIndex] = useState(null); + const containerRef = useRef(null); + const hoverTimeoutRef = useRef(null); + const isTouch = useMediaQuery("(hover: none)"); + + useEffect(() => { + const handleOutsideClick = (e: MouseEvent) => { + if ( + pinnedIndex !== null && + containerRef.current && + !containerRef.current.contains(e.target as Node) + ) { + setPinnedIndex(null); + } + }; + + if (pinnedIndex !== null) { + document.addEventListener("mousedown", handleOutsideClick); + return () => + document.removeEventListener("mousedown", handleOutsideClick); + } + }, [pinnedIndex]); + + useEffect(() => { + if (focusedIndex !== null && containerRef.current) { + const buttons = containerRef.current.querySelectorAll('[role="button"]'); + const targetButton = buttons[focusedIndex] as HTMLElement; + if (targetButton) { + targetButton.focus(); + } + } + }, [focusedIndex]); + + useEffect(() => { + return () => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + }; + }, []); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + setPinnedIndex(null); + setFocusedIndex(null); + setHoveredIndex(null); + + if (focusedIndex !== null) { + const buttons = + containerRef.current?.querySelectorAll('[role="button"]'); + const button = buttons?.[focusedIndex] as HTMLElement; + if (button) { + button.blur(); + } + } + + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + return; + } + + if (focusedIndex !== null) { + switch (e.key) { + case "ArrowLeft": + e.preventDefault(); + setFocusedIndex((prev) => + prev !== null && prev > 0 ? prev - 1 : data.length - 1, + ); + break; + case "ArrowRight": + e.preventDefault(); + setFocusedIndex((prev) => + prev !== null && prev < data.length - 1 ? prev + 1 : 0, + ); + break; + case "ArrowUp": + e.preventDefault(); + { + const prevMonitor = containerRef.current?.closest( + '[data-slot="status-monitor"]', + )?.previousElementSibling; + if (prevMonitor) { + const prevTracker = prevMonitor.querySelector('[role="toolbar"]'); + if (prevTracker) { + const buttons = + prevTracker.querySelectorAll('[role="button"]'); + const button = buttons?.[focusedIndex] as HTMLElement; + if (button) { + button.focus(); + } + } + } + } + break; + case "ArrowDown": + e.preventDefault(); + { + const nextMonitor = containerRef.current?.closest( + '[data-slot="status-monitor"]', + )?.nextElementSibling; + if (nextMonitor) { + const nextTracker = nextMonitor.querySelector('[role="toolbar"]'); + if (nextTracker) { + const buttons = + nextTracker.querySelectorAll('[role="button"]'); + const button = buttons?.[focusedIndex] as HTMLElement; + if (button) { + button.focus(); + } + } + } + } + break; + case "Enter": + case "Escape": + case " ": + e.preventDefault(); + handleBarClick(focusedIndex); + break; + } + } + }; + + const handleBarClick = (index: number) => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + if (pinnedIndex === index) { + setPinnedIndex(null); + } else { + setPinnedIndex(index); + } + }; + + const handleBarFocus = (index: number) => { + setFocusedIndex(index); + }; + + const handleBarBlur = (e: React.FocusEvent, _currentIndex: number) => { + const relatedTarget = e.relatedTarget as HTMLElement; + const isMovingToAnotherBar = + relatedTarget && + relatedTarget.closest('[role="toolbar"]') === containerRef.current && + relatedTarget.getAttribute("role") === "button"; + + if (!isMovingToAnotherBar) { + setFocusedIndex(null); + } + }; + + const handleBarMouseEnter = (index: number) => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + setHoveredIndex(index); + }; + + const handleBarMouseLeave = () => { + hoverTimeoutRef.current = setTimeout(() => { + setHoveredIndex(null); + }, 100); + }; + + const handleHoverCardMouseEnter = () => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + }; + + const handleHoverCardMouseLeave = () => { + setHoveredIndex(null); + }; + + return ( +
+ {data.map((item, index) => { + const isPinned = pinnedIndex === index; + const isFocused = focusedIndex === index; + const isHovered = hoveredIndex === index; + + return ( + + +
handleBarClick(index)} + onFocus={() => handleBarFocus(index)} + onBlur={(e) => handleBarBlur(e, index)} + onMouseEnter={() => handleBarMouseEnter(index)} + onMouseLeave={handleBarMouseLeave} + tabIndex={ + index === data.length - 1 && focusedIndex === null + ? 0 + : isFocused + ? 0 + : -1 + } + role="button" + aria-label={`Day ${index + 1} status`} + aria-pressed={isPinned} + > + {item.bar.map((segment, segmentIndex) => ( +
+ ))} +
+ + +
+
+ {new Date(item.day).toLocaleDateString("default", { + day: "numeric", + month: "short", + year: "numeric", + })} +
+ +
+ {item.card.map((cardItem, cardIndex) => ( + + ))} +
+ {item.events.length > 0 && ( + <> + +
+ {item.events.map((event) => { + const eventStatus = + event.type === "incident" + ? "error" + : event.type === "report" + ? "degraded" + : "info"; + + return ( + + ); + })} +
+ + )} + {isPinned && !isTouch && ( + <> + +
+ Click again to unpin + Esc +
+ + )} +
+
+ + ); + })} +
+ ); +} + +function StatusTrackerContentStatic({ + status, + value, +}: { + status: "success" | "degraded" | "error" | "info" | "empty"; + value: string; +}) { + return ( +
+
+
+
{staticLabels[status]}
+
+
+ {value} +
+
+ ); +} + +function StatusTrackerEventStatic({ + name, + from, + to, + status, +}: { + name: string; + from?: Date | null; + to?: Date | null; + status: "success" | "degraded" | "error" | "info" | "empty"; +}) { + if (!from) return null; + + return ( +
+
+
+
+
+
{name}
+
+
+
+ {formatDateRange(from, to ?? undefined)}{" "} + + {formatDuration({ from, to, name, status })} + +
+
+ ); +} + +const formatDuration = ({ + from, + to, + name, +}: { + name: string; + from?: Date | null; + to?: Date | null; + status: "success" | "degraded" | "error" | "info" | "empty"; +}) => { + if (!from) return null; + if (!to) return "ongoing"; + const duration = formatDistanceStrict(from, to); + const isMultipleIncidents = name.includes("Downtime ("); + if (isMultipleIncidents) return `across ${duration}`; + if (duration === "0 seconds") return null; + return duration; +}; diff --git a/apps/status-page/src/components/status-page/status-events.tsx b/apps/status-page/src/components/status-page/status-events.tsx index e9fbdcd958..8eb2ce174e 100644 --- a/apps/status-page/src/components/status-page/status-events.tsx +++ b/apps/status-page/src/components/status-page/status-events.tsx @@ -1,6 +1,5 @@ import { ProcessMessage } from "@/components/content/process-message"; import { TimestampHoverCard } from "@/components/content/timestamp-hover-card"; -import { usePathnamePrefix } from "@/hooks/use-pathname-prefix"; import { formatDate, formatDateRange, formatDateTime } from "@/lib/formatter"; import { Badge } from "@openstatus/ui/components/ui/badge"; import { Separator } from "@openstatus/ui/components/ui/separator"; @@ -187,11 +186,9 @@ export function StatusEventTimelineReport({ withDot?: boolean; maxUpdates?: number; }) { - const _prefix = usePathnamePrefix(); const sortedUpdates = [...updates].sort( (a, b) => b.date.getTime() - a.date.getTime(), ); - const _hasMoreUpdates = maxUpdates && sortedUpdates.length > maxUpdates; const displayedUpdates = maxUpdates ? sortedUpdates.slice(0, maxUpdates) : sortedUpdates; diff --git a/apps/status-page/src/i18n/request.ts b/apps/status-page/src/i18n/request.ts index 3d4ad8b315..0b85ab8b34 100644 --- a/apps/status-page/src/i18n/request.ts +++ b/apps/status-page/src/i18n/request.ts @@ -4,12 +4,34 @@ import { routing } from "./routing"; export default getRequestConfig(async ({ requestLocale }) => { let locale = await requestLocale; - if (!locale || !routing.locales.includes(locale as any)) { + if (!locale || !(routing.locales as readonly string[]).includes(locale)) { locale = routing.defaultLocale; } return { locale, messages: (await import(`../../messages/${locale}.json`)).default, + + experimental: { + // Relative path(s) to source files + srcPath: './src', + + extract: { + // Defines which locale to extract to + sourceLocale: 'en' + }, + + messages: { + // Relative path to the directory + path: './messages', + + // Either 'json', 'po', or a custom format (see below) + format: 'json', + + // Either 'infer' to automatically detect locales based on + // matching files in `path` or an explicit array of locales + locales: 'infer' + } + } }; }); From 44594f4329aa15867be78e5eb979c4afa441ec1d Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Thu, 12 Mar 2026 00:47:20 +0530 Subject: [PATCH 13/24] Update the extracted config and proxy fix Signed-off-by: Moulik Aggarwal --- apps/status-page/messages/de.json | 92 ++++++++++++++++++++++- apps/status-page/messages/en.json | 92 ++++++++++++++++++++++- apps/status-page/messages/es.json | 92 ++++++++++++++++++++++- apps/status-page/messages/fr.json | 92 ++++++++++++++++++++++- apps/status-page/messages/ja.json | 92 ++++++++++++++++++++++- apps/status-page/messages/ko.json | 92 ++++++++++++++++++++++- apps/status-page/messages/pt.json | 92 ++++++++++++++++++++++- apps/status-page/messages/zh.json | 92 ++++++++++++++++++++++- apps/status-page/next.config.ts | 15 +++- apps/status-page/src/i18n/config.ts | 8 +- apps/status-page/src/i18n/request.ts | 22 ------ apps/status-page/src/i18n/routing.ts | 2 +- apps/status-page/src/proxy.ts | 108 +++++++++++++++++---------- 13 files changed, 813 insertions(+), 78 deletions(-) diff --git a/apps/status-page/messages/de.json b/apps/status-page/messages/de.json index 0967ef424b..f0743b8f21 100644 --- a/apps/status-page/messages/de.json +++ b/apps/status-page/messages/de.json @@ -1 +1,91 @@ -{} +{ + "Y9HHck": "", + "1QcGkA": "", + "wSZR47": "", + "txkW56": "", + "OrFVks": "", + "n36zhX": "", + "qIAQSi": "", + "lbw10C": "", + "t262xH": "", + "DwevKz": "", + "Ppx673": "", + "JCMXwP": "", + "I7B7SH": "", + "OSI607": "", + "a9S/OH": "", + "HSv9BP": "", + "VL1Y/1": "", + "Ew1f8q": "", + "awr0AJ": "", + "CVsoUM": "", + "BRGcS0": "", + "9vqdq3": "", + "fFOayY": "", + "u81G9+": "", + "i2FBWn": "", + "G5Lt80": "", + "YV7rXP": "", + "6zzIEm": "", + "6pCzRs": "", + "zL23+z": "", + "79eRW1": "", + "dX7+Rv": "", + "m0fapd": "", + "sy+pv5": "", + "9Utk00": "", + "Eq5gCU": "", + "qp+wDV": "", + "d/jCcy": "", + "FlVuUh": "", + "8aUjqQ": "", + "5sg7KC": "", + "IGY48m": "", + "Pgb3Xj": "", + "WOH7Yj": "", + "L7z2/k": "", + "NOyDVq": "", + "tzMNF3": "", + "ZvKSfJ": "", + "xJrRMG": "", + "tKMlOc": "", + "krEziQ": "", + "BQBZU+": "", + "b9fOA1": "", + "80EXUh": "", + "dudqv/": "", + "2syGZB": "", + "W6nSYE": "", + "1P6GMj": "", + "7cv4Uf": "", + "/GKH/w": "", + "2K7gg0": "", + "FDReLp": "", + "qDj0JR": "", + "CYs0LF": "", + "kkpP2k": "", + "Dnob31": "", + "VQDmmK": "", + "JOZGPR": "", + "GbVCQb": "", + "myq2ZL": "", + "KN7zKn": "", + "D3rOMr": "", + "uPb/gh": "", + "sjzDbu": "", + "q0qMyV": "", + "9y9QQh": "", + "waUHa4": "", + "cVqFq/": "", + "gczcC5": "", + "8OoV56": "", + "Auj/Ki": "", + "SyYroX": "", + "PSqtlY": "", + "rptmhC": "", + "2yCGR2": "", + "u5aHb4": "", + "p556q3": "", + "4l6vz1": "", + "45YlLU": "" +} diff --git a/apps/status-page/messages/en.json b/apps/status-page/messages/en.json index 0967ef424b..c22b195125 100644 --- a/apps/status-page/messages/en.json +++ b/apps/status-page/messages/en.json @@ -1 +1,91 @@ -{} +{ + "Y9HHck": "Authenticate", + "1QcGkA": "Enter your email to receive a magic link for accessing the status page. Note: Only emails from approved domains are accepted.", + "wSZR47": "Submit", + "txkW56": "Submitting...", + "OrFVks": "Check your inbox!", + "n36zhX": "Access the status page by clicking the link in the email.", + "qIAQSi": "Protected Page", + "lbw10C": "Enter the password to access the status page.", + "t262xH": "Email and redirectTo are required", + "DwevKz": "An unexpected error occurred during sign in", + "Ppx673": "Reports", + "JCMXwP": "Maintenances", + "I7B7SH": "No maintenances found", + "OSI607": "No maintenances found for this status page.", + "a9S/OH": "Maintenance not found", + "HSv9BP": "The maintenance you are looking for does not exist.", + "VL1Y/1": "Report not found", + "Ew1f8q": "The report you are looking for does not exist.", + "awr0AJ": "Monitor not found", + "CVsoUM": "The monitor you are looking for does not exist.", + "BRGcS0": "Global Latency", + "9vqdq3": "Region Latency", + "fFOayY": "regions", + "u81G9+": "Uptime", + "i2FBWn": "checks", + "G5Lt80": "The aggregated latency from all active regions based on different quantiles.", + "YV7rXP": "Latency by Region", + "6zzIEm": "Region latency per p75 quantile, sorted by slowest region. Compare up to 6 regions.", + "6pCzRs": "Total Uptime", + "zL23+z": "Main values of uptime and availability, transparent.", + "79eRW1": "Confirming...", + "dX7+Rv": "Confirmed", + "m0fapd": "Failed to confirm", + "sy+pv5": "Email", + "9Utk00": "Updating subscription...", + "Eq5gCU": "Subscription updated", + "qp+wDV": "Failed to update subscription", + "d/jCcy": "Subscribe to specific components", + "FlVuUh": "No components to subscribe to", + "8aUjqQ": "This status page has no components to subscribe to.", + "5sg7KC": "Password", + "IGY48m": "Subscribing...", + "Pgb3Xj": "Subscribed", + "WOH7Yj": "Failed to subscribe", + "L7z2/k": "This page has no components to subscribe to.", + "NOyDVq": "powered by", + "tzMNF3": "Status", + "ZvKSfJ": "Events", + "xJrRMG": "Monitors", + "tKMlOc": "Menu", + "krEziQ": "Get in touch", + "BQBZU+": "All Systems Operational", + "b9fOA1": "Degraded Performance", + "80EXUh": "Downtime Performance", + "dudqv/": "Maintenance", + "2syGZB": "Report resolved", + "W6nSYE": "Resolved", + "1P6GMj": "Monitoring", + "7cv4Uf": "Identified", + "/GKH/w": "Investigating", + "2K7gg0": "View full report", + "FDReLp": "No recent notifications", + "qDj0JR": "There have been no reports within the last 7 days.", + "CYs0LF": "View events history", + "kkpP2k": "today", + "Dnob31": "Operational", + "VQDmmK": "Degraded", + "JOZGPR": "Downtime", + "GbVCQb": "Click again to unpin", + "myq2ZL": "Normal", + "KN7zKn": "Error", + "D3rOMr": "No Data", + "uPb/gh": "Get updates", + "sjzDbu": "Slack", + "q0qMyV": "RSS", + "9y9QQh": "JSON", + "waUHa4": "SSH", + "cVqFq/": "Get email notifications whenever a report has been created or resolved", + "gczcC5": "Subscribe", + "8OoV56": "Get the RSS feed", + "Auj/Ki": "Get the Atom feed", + "SyYroX": "Get the JSON updates", + "PSqtlY": "Get status via SSH", + "rptmhC": "For status updates in Slack, paste the text below into any channel.", + "2yCGR2": "Link copied to clipboard", + "u5aHb4": "Copy Link", + "p556q3": "Copied", + "4l6vz1": "Copy", + "45YlLU": "Validate your email to receive updates and you are all set." +} diff --git a/apps/status-page/messages/es.json b/apps/status-page/messages/es.json index 0967ef424b..f0743b8f21 100644 --- a/apps/status-page/messages/es.json +++ b/apps/status-page/messages/es.json @@ -1 +1,91 @@ -{} +{ + "Y9HHck": "", + "1QcGkA": "", + "wSZR47": "", + "txkW56": "", + "OrFVks": "", + "n36zhX": "", + "qIAQSi": "", + "lbw10C": "", + "t262xH": "", + "DwevKz": "", + "Ppx673": "", + "JCMXwP": "", + "I7B7SH": "", + "OSI607": "", + "a9S/OH": "", + "HSv9BP": "", + "VL1Y/1": "", + "Ew1f8q": "", + "awr0AJ": "", + "CVsoUM": "", + "BRGcS0": "", + "9vqdq3": "", + "fFOayY": "", + "u81G9+": "", + "i2FBWn": "", + "G5Lt80": "", + "YV7rXP": "", + "6zzIEm": "", + "6pCzRs": "", + "zL23+z": "", + "79eRW1": "", + "dX7+Rv": "", + "m0fapd": "", + "sy+pv5": "", + "9Utk00": "", + "Eq5gCU": "", + "qp+wDV": "", + "d/jCcy": "", + "FlVuUh": "", + "8aUjqQ": "", + "5sg7KC": "", + "IGY48m": "", + "Pgb3Xj": "", + "WOH7Yj": "", + "L7z2/k": "", + "NOyDVq": "", + "tzMNF3": "", + "ZvKSfJ": "", + "xJrRMG": "", + "tKMlOc": "", + "krEziQ": "", + "BQBZU+": "", + "b9fOA1": "", + "80EXUh": "", + "dudqv/": "", + "2syGZB": "", + "W6nSYE": "", + "1P6GMj": "", + "7cv4Uf": "", + "/GKH/w": "", + "2K7gg0": "", + "FDReLp": "", + "qDj0JR": "", + "CYs0LF": "", + "kkpP2k": "", + "Dnob31": "", + "VQDmmK": "", + "JOZGPR": "", + "GbVCQb": "", + "myq2ZL": "", + "KN7zKn": "", + "D3rOMr": "", + "uPb/gh": "", + "sjzDbu": "", + "q0qMyV": "", + "9y9QQh": "", + "waUHa4": "", + "cVqFq/": "", + "gczcC5": "", + "8OoV56": "", + "Auj/Ki": "", + "SyYroX": "", + "PSqtlY": "", + "rptmhC": "", + "2yCGR2": "", + "u5aHb4": "", + "p556q3": "", + "4l6vz1": "", + "45YlLU": "" +} diff --git a/apps/status-page/messages/fr.json b/apps/status-page/messages/fr.json index 0967ef424b..f0743b8f21 100644 --- a/apps/status-page/messages/fr.json +++ b/apps/status-page/messages/fr.json @@ -1 +1,91 @@ -{} +{ + "Y9HHck": "", + "1QcGkA": "", + "wSZR47": "", + "txkW56": "", + "OrFVks": "", + "n36zhX": "", + "qIAQSi": "", + "lbw10C": "", + "t262xH": "", + "DwevKz": "", + "Ppx673": "", + "JCMXwP": "", + "I7B7SH": "", + "OSI607": "", + "a9S/OH": "", + "HSv9BP": "", + "VL1Y/1": "", + "Ew1f8q": "", + "awr0AJ": "", + "CVsoUM": "", + "BRGcS0": "", + "9vqdq3": "", + "fFOayY": "", + "u81G9+": "", + "i2FBWn": "", + "G5Lt80": "", + "YV7rXP": "", + "6zzIEm": "", + "6pCzRs": "", + "zL23+z": "", + "79eRW1": "", + "dX7+Rv": "", + "m0fapd": "", + "sy+pv5": "", + "9Utk00": "", + "Eq5gCU": "", + "qp+wDV": "", + "d/jCcy": "", + "FlVuUh": "", + "8aUjqQ": "", + "5sg7KC": "", + "IGY48m": "", + "Pgb3Xj": "", + "WOH7Yj": "", + "L7z2/k": "", + "NOyDVq": "", + "tzMNF3": "", + "ZvKSfJ": "", + "xJrRMG": "", + "tKMlOc": "", + "krEziQ": "", + "BQBZU+": "", + "b9fOA1": "", + "80EXUh": "", + "dudqv/": "", + "2syGZB": "", + "W6nSYE": "", + "1P6GMj": "", + "7cv4Uf": "", + "/GKH/w": "", + "2K7gg0": "", + "FDReLp": "", + "qDj0JR": "", + "CYs0LF": "", + "kkpP2k": "", + "Dnob31": "", + "VQDmmK": "", + "JOZGPR": "", + "GbVCQb": "", + "myq2ZL": "", + "KN7zKn": "", + "D3rOMr": "", + "uPb/gh": "", + "sjzDbu": "", + "q0qMyV": "", + "9y9QQh": "", + "waUHa4": "", + "cVqFq/": "", + "gczcC5": "", + "8OoV56": "", + "Auj/Ki": "", + "SyYroX": "", + "PSqtlY": "", + "rptmhC": "", + "2yCGR2": "", + "u5aHb4": "", + "p556q3": "", + "4l6vz1": "", + "45YlLU": "" +} diff --git a/apps/status-page/messages/ja.json b/apps/status-page/messages/ja.json index 0967ef424b..f0743b8f21 100644 --- a/apps/status-page/messages/ja.json +++ b/apps/status-page/messages/ja.json @@ -1 +1,91 @@ -{} +{ + "Y9HHck": "", + "1QcGkA": "", + "wSZR47": "", + "txkW56": "", + "OrFVks": "", + "n36zhX": "", + "qIAQSi": "", + "lbw10C": "", + "t262xH": "", + "DwevKz": "", + "Ppx673": "", + "JCMXwP": "", + "I7B7SH": "", + "OSI607": "", + "a9S/OH": "", + "HSv9BP": "", + "VL1Y/1": "", + "Ew1f8q": "", + "awr0AJ": "", + "CVsoUM": "", + "BRGcS0": "", + "9vqdq3": "", + "fFOayY": "", + "u81G9+": "", + "i2FBWn": "", + "G5Lt80": "", + "YV7rXP": "", + "6zzIEm": "", + "6pCzRs": "", + "zL23+z": "", + "79eRW1": "", + "dX7+Rv": "", + "m0fapd": "", + "sy+pv5": "", + "9Utk00": "", + "Eq5gCU": "", + "qp+wDV": "", + "d/jCcy": "", + "FlVuUh": "", + "8aUjqQ": "", + "5sg7KC": "", + "IGY48m": "", + "Pgb3Xj": "", + "WOH7Yj": "", + "L7z2/k": "", + "NOyDVq": "", + "tzMNF3": "", + "ZvKSfJ": "", + "xJrRMG": "", + "tKMlOc": "", + "krEziQ": "", + "BQBZU+": "", + "b9fOA1": "", + "80EXUh": "", + "dudqv/": "", + "2syGZB": "", + "W6nSYE": "", + "1P6GMj": "", + "7cv4Uf": "", + "/GKH/w": "", + "2K7gg0": "", + "FDReLp": "", + "qDj0JR": "", + "CYs0LF": "", + "kkpP2k": "", + "Dnob31": "", + "VQDmmK": "", + "JOZGPR": "", + "GbVCQb": "", + "myq2ZL": "", + "KN7zKn": "", + "D3rOMr": "", + "uPb/gh": "", + "sjzDbu": "", + "q0qMyV": "", + "9y9QQh": "", + "waUHa4": "", + "cVqFq/": "", + "gczcC5": "", + "8OoV56": "", + "Auj/Ki": "", + "SyYroX": "", + "PSqtlY": "", + "rptmhC": "", + "2yCGR2": "", + "u5aHb4": "", + "p556q3": "", + "4l6vz1": "", + "45YlLU": "" +} diff --git a/apps/status-page/messages/ko.json b/apps/status-page/messages/ko.json index 0967ef424b..f0743b8f21 100644 --- a/apps/status-page/messages/ko.json +++ b/apps/status-page/messages/ko.json @@ -1 +1,91 @@ -{} +{ + "Y9HHck": "", + "1QcGkA": "", + "wSZR47": "", + "txkW56": "", + "OrFVks": "", + "n36zhX": "", + "qIAQSi": "", + "lbw10C": "", + "t262xH": "", + "DwevKz": "", + "Ppx673": "", + "JCMXwP": "", + "I7B7SH": "", + "OSI607": "", + "a9S/OH": "", + "HSv9BP": "", + "VL1Y/1": "", + "Ew1f8q": "", + "awr0AJ": "", + "CVsoUM": "", + "BRGcS0": "", + "9vqdq3": "", + "fFOayY": "", + "u81G9+": "", + "i2FBWn": "", + "G5Lt80": "", + "YV7rXP": "", + "6zzIEm": "", + "6pCzRs": "", + "zL23+z": "", + "79eRW1": "", + "dX7+Rv": "", + "m0fapd": "", + "sy+pv5": "", + "9Utk00": "", + "Eq5gCU": "", + "qp+wDV": "", + "d/jCcy": "", + "FlVuUh": "", + "8aUjqQ": "", + "5sg7KC": "", + "IGY48m": "", + "Pgb3Xj": "", + "WOH7Yj": "", + "L7z2/k": "", + "NOyDVq": "", + "tzMNF3": "", + "ZvKSfJ": "", + "xJrRMG": "", + "tKMlOc": "", + "krEziQ": "", + "BQBZU+": "", + "b9fOA1": "", + "80EXUh": "", + "dudqv/": "", + "2syGZB": "", + "W6nSYE": "", + "1P6GMj": "", + "7cv4Uf": "", + "/GKH/w": "", + "2K7gg0": "", + "FDReLp": "", + "qDj0JR": "", + "CYs0LF": "", + "kkpP2k": "", + "Dnob31": "", + "VQDmmK": "", + "JOZGPR": "", + "GbVCQb": "", + "myq2ZL": "", + "KN7zKn": "", + "D3rOMr": "", + "uPb/gh": "", + "sjzDbu": "", + "q0qMyV": "", + "9y9QQh": "", + "waUHa4": "", + "cVqFq/": "", + "gczcC5": "", + "8OoV56": "", + "Auj/Ki": "", + "SyYroX": "", + "PSqtlY": "", + "rptmhC": "", + "2yCGR2": "", + "u5aHb4": "", + "p556q3": "", + "4l6vz1": "", + "45YlLU": "" +} diff --git a/apps/status-page/messages/pt.json b/apps/status-page/messages/pt.json index 0967ef424b..f0743b8f21 100644 --- a/apps/status-page/messages/pt.json +++ b/apps/status-page/messages/pt.json @@ -1 +1,91 @@ -{} +{ + "Y9HHck": "", + "1QcGkA": "", + "wSZR47": "", + "txkW56": "", + "OrFVks": "", + "n36zhX": "", + "qIAQSi": "", + "lbw10C": "", + "t262xH": "", + "DwevKz": "", + "Ppx673": "", + "JCMXwP": "", + "I7B7SH": "", + "OSI607": "", + "a9S/OH": "", + "HSv9BP": "", + "VL1Y/1": "", + "Ew1f8q": "", + "awr0AJ": "", + "CVsoUM": "", + "BRGcS0": "", + "9vqdq3": "", + "fFOayY": "", + "u81G9+": "", + "i2FBWn": "", + "G5Lt80": "", + "YV7rXP": "", + "6zzIEm": "", + "6pCzRs": "", + "zL23+z": "", + "79eRW1": "", + "dX7+Rv": "", + "m0fapd": "", + "sy+pv5": "", + "9Utk00": "", + "Eq5gCU": "", + "qp+wDV": "", + "d/jCcy": "", + "FlVuUh": "", + "8aUjqQ": "", + "5sg7KC": "", + "IGY48m": "", + "Pgb3Xj": "", + "WOH7Yj": "", + "L7z2/k": "", + "NOyDVq": "", + "tzMNF3": "", + "ZvKSfJ": "", + "xJrRMG": "", + "tKMlOc": "", + "krEziQ": "", + "BQBZU+": "", + "b9fOA1": "", + "80EXUh": "", + "dudqv/": "", + "2syGZB": "", + "W6nSYE": "", + "1P6GMj": "", + "7cv4Uf": "", + "/GKH/w": "", + "2K7gg0": "", + "FDReLp": "", + "qDj0JR": "", + "CYs0LF": "", + "kkpP2k": "", + "Dnob31": "", + "VQDmmK": "", + "JOZGPR": "", + "GbVCQb": "", + "myq2ZL": "", + "KN7zKn": "", + "D3rOMr": "", + "uPb/gh": "", + "sjzDbu": "", + "q0qMyV": "", + "9y9QQh": "", + "waUHa4": "", + "cVqFq/": "", + "gczcC5": "", + "8OoV56": "", + "Auj/Ki": "", + "SyYroX": "", + "PSqtlY": "", + "rptmhC": "", + "2yCGR2": "", + "u5aHb4": "", + "p556q3": "", + "4l6vz1": "", + "45YlLU": "" +} diff --git a/apps/status-page/messages/zh.json b/apps/status-page/messages/zh.json index 0967ef424b..f0743b8f21 100644 --- a/apps/status-page/messages/zh.json +++ b/apps/status-page/messages/zh.json @@ -1 +1,91 @@ -{} +{ + "Y9HHck": "", + "1QcGkA": "", + "wSZR47": "", + "txkW56": "", + "OrFVks": "", + "n36zhX": "", + "qIAQSi": "", + "lbw10C": "", + "t262xH": "", + "DwevKz": "", + "Ppx673": "", + "JCMXwP": "", + "I7B7SH": "", + "OSI607": "", + "a9S/OH": "", + "HSv9BP": "", + "VL1Y/1": "", + "Ew1f8q": "", + "awr0AJ": "", + "CVsoUM": "", + "BRGcS0": "", + "9vqdq3": "", + "fFOayY": "", + "u81G9+": "", + "i2FBWn": "", + "G5Lt80": "", + "YV7rXP": "", + "6zzIEm": "", + "6pCzRs": "", + "zL23+z": "", + "79eRW1": "", + "dX7+Rv": "", + "m0fapd": "", + "sy+pv5": "", + "9Utk00": "", + "Eq5gCU": "", + "qp+wDV": "", + "d/jCcy": "", + "FlVuUh": "", + "8aUjqQ": "", + "5sg7KC": "", + "IGY48m": "", + "Pgb3Xj": "", + "WOH7Yj": "", + "L7z2/k": "", + "NOyDVq": "", + "tzMNF3": "", + "ZvKSfJ": "", + "xJrRMG": "", + "tKMlOc": "", + "krEziQ": "", + "BQBZU+": "", + "b9fOA1": "", + "80EXUh": "", + "dudqv/": "", + "2syGZB": "", + "W6nSYE": "", + "1P6GMj": "", + "7cv4Uf": "", + "/GKH/w": "", + "2K7gg0": "", + "FDReLp": "", + "qDj0JR": "", + "CYs0LF": "", + "kkpP2k": "", + "Dnob31": "", + "VQDmmK": "", + "JOZGPR": "", + "GbVCQb": "", + "myq2ZL": "", + "KN7zKn": "", + "D3rOMr": "", + "uPb/gh": "", + "sjzDbu": "", + "q0qMyV": "", + "9y9QQh": "", + "waUHa4": "", + "cVqFq/": "", + "gczcC5": "", + "8OoV56": "", + "Auj/Ki": "", + "SyYroX": "", + "PSqtlY": "", + "rptmhC": "", + "2yCGR2": "", + "u5aHb4": "", + "p556q3": "", + "4l6vz1": "", + "45YlLU": "" +} diff --git a/apps/status-page/next.config.ts b/apps/status-page/next.config.ts index d2871e8013..c73949080d 100644 --- a/apps/status-page/next.config.ts +++ b/apps/status-page/next.config.ts @@ -2,7 +2,20 @@ import { withSentryConfig } from "@sentry/nextjs"; import type { NextConfig } from "next"; import createNextIntlPlugin from "next-intl/plugin"; -const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); +const withNextIntl = createNextIntlPlugin({ + requestConfig: "./src/i18n/request.ts", + experimental: { + srcPath: "./src", + extract: { + sourceLocale: "en", + }, + messages: { + path: "./messages", + format: "json", + locales: "infer", + }, + }, +}); const nextConfig: NextConfig = { output: process.env.SELF_HOST === "true" ? "standalone" : undefined, diff --git a/apps/status-page/src/i18n/config.ts b/apps/status-page/src/i18n/config.ts index e9f9612204..4b89929000 100644 --- a/apps/status-page/src/i18n/config.ts +++ b/apps/status-page/src/i18n/config.ts @@ -1,12 +1,6 @@ export const locales = [ "en", - "es", - "fr", - "de", - "pt", - "ja", - "zh", - "ko", + "fr" ] as const; export type Locale = (typeof locales)[number]; export const defaultLocale: Locale = "en"; diff --git a/apps/status-page/src/i18n/request.ts b/apps/status-page/src/i18n/request.ts index 0b85ab8b34..b59fbdd409 100644 --- a/apps/status-page/src/i18n/request.ts +++ b/apps/status-page/src/i18n/request.ts @@ -11,27 +11,5 @@ export default getRequestConfig(async ({ requestLocale }) => { return { locale, messages: (await import(`../../messages/${locale}.json`)).default, - - experimental: { - // Relative path(s) to source files - srcPath: './src', - - extract: { - // Defines which locale to extract to - sourceLocale: 'en' - }, - - messages: { - // Relative path to the directory - path: './messages', - - // Either 'json', 'po', or a custom format (see below) - format: 'json', - - // Either 'infer' to automatically detect locales based on - // matching files in `path` or an explicit array of locales - locales: 'infer' - } - } }; }); diff --git a/apps/status-page/src/i18n/routing.ts b/apps/status-page/src/i18n/routing.ts index be6bdc7c2b..44721fe073 100644 --- a/apps/status-page/src/i18n/routing.ts +++ b/apps/status-page/src/i18n/routing.ts @@ -4,5 +4,5 @@ import { defaultLocale, locales } from "./config"; export const routing = defineRouting({ locales, defaultLocale, - localePrefix: "as-needed", + localePrefix: "never", }); diff --git a/apps/status-page/src/proxy.ts b/apps/status-page/src/proxy.ts index 9fc6ca8a98..db1a1dfbfe 100644 --- a/apps/status-page/src/proxy.ts +++ b/apps/status-page/src/proxy.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; +import { defaultLocale, locales } from "@/i18n/config"; import { auth } from "@/lib/auth"; import { db, sql } from "@openstatus/db"; @@ -7,6 +8,13 @@ import { page, selectPageSchema } from "@openstatus/db/src/schema"; import { getValidSubdomain } from "./lib/domain"; import { createProtectedCookieKey } from "./lib/protected"; +function isValidLocale(segment: string | undefined): segment is string { + return ( + !!segment && + (locales as readonly string[]).includes(segment.toLowerCase() as never) + ); +} + export default auth(async (req) => { const url = req.nextUrl.clone(); const response = NextResponse.next(); @@ -51,6 +59,8 @@ export default auth(async (req) => { return response; } + console.log("page subdomain", page); + const query = await db .select() .from(page) @@ -69,6 +79,37 @@ export default auth(async (req) => { console.log({ slug: _page?.slug, customDomain: _page?.customDomain }); + // --- Locale detection and redirect --- + // For pathname type: URL is /{slug}/{locale}/... → locale at index 2 + // For hostname type: URL is /{locale}/... → locale at index 1 + const localeIndex = type === "pathname" ? 2 : 1; + const localeSegment = pathnames[localeIndex]?.toLowerCase(); + + if (!isValidLocale(localeSegment)) { + // Redirect to insert default locale into the URL + if (type === "pathname") { + // /slug/rest... → /slug/{defaultLocale}/rest... + const rest = pathnames.slice(2).filter(Boolean).join("/"); + const redirectUrl = new URL( + `/${prefix}/${defaultLocale}${rest ? `/${rest}` : ""}`, + req.url, + ); + redirectUrl.search = url.search; + return NextResponse.redirect(redirectUrl); + } + // hostname: /rest... → /{defaultLocale}/rest... + const rest = pathnames.slice(1).filter(Boolean).join("/"); + const redirectUrl = new URL( + `/${defaultLocale}${rest ? `/${rest}` : ""}`, + req.url, + ); + redirectUrl.search = url.search; + return NextResponse.redirect(redirectUrl); + } + + const currentLocale = localeSegment; + + // --- Password protection --- if (_page?.accessType === "password") { const protectedCookie = cookies.get(createProtectedCookieKey(_page.slug)); const cookiePassword = protectedCookie ? protectedCookie.value : undefined; @@ -81,42 +122,47 @@ export default auth(async (req) => { // custom domain redirect if (_page.customDomain && host !== `${_page.slug}.stpg.dev`) { const redirect = pathname.replace(`/${_page.customDomain}`, ""); - const url = new URL( - `https://${_page.customDomain}/login?redirect=${encodeURIComponent( + const loginUrl = new URL( + `https://${_page.customDomain}/${currentLocale}/login?redirect=${encodeURIComponent( redirect, )}`, ); - console.log("redirect to /login", url.toString()); - return NextResponse.redirect(url); + console.log("redirect to /login", loginUrl.toString()); + return NextResponse.redirect(loginUrl); } - const url = new URL( + const loginUrl = new URL( `${origin}${ type === "pathname" ? `/${prefix}` : "" - }/login?redirect=${encodeURIComponent(pathname)}`, + }/${currentLocale}/login?redirect=${encodeURIComponent(pathname)}`, ); - return NextResponse.redirect(url); + return NextResponse.redirect(loginUrl); } if (password === _page.password && url.pathname.endsWith("/login")) { const redirect = url.searchParams.get("redirect"); // custom domain redirect if (_page.customDomain && host !== `${_page.slug}.stpg.dev`) { - const url = new URL(`https://${_page.customDomain}${redirect ?? "/"}`); - console.log("redirect to /", url.toString()); - return NextResponse.redirect(url); + const redirectUrl = new URL( + `https://${_page.customDomain}${redirect ?? `/${currentLocale}`}`, + ); + console.log("redirect to /", redirectUrl.toString()); + return NextResponse.redirect(redirectUrl); } return NextResponse.redirect( new URL( `${req.nextUrl.origin}${ - redirect ?? type === "pathname" ? `/${prefix}` : "/" + redirect ?? type === "pathname" + ? `/${prefix}/${currentLocale}` + : `/${currentLocale}` }`, ), ); } } + // --- Email-domain protection --- if (_page.accessType === "email-domain") { const { origin, pathname } = req.nextUrl; const email = req.auth?.user?.email; @@ -125,23 +171,24 @@ export default auth(async (req) => { !pathname.endsWith("/login") && (!emailDomain || !_page.authEmailDomains.includes(emailDomain)) ) { - const url = new URL( - `${origin}${type === "pathname" ? `/${prefix}` : ""}/login`, + const loginUrl = new URL( + `${origin}${type === "pathname" ? `/${prefix}` : ""}/${currentLocale}/login`, ); - return NextResponse.redirect(url); + return NextResponse.redirect(loginUrl); } if ( pathname.endsWith("/login") && emailDomain && _page.authEmailDomains.includes(emailDomain) ) { - const url = new URL( - `${origin}${type === "pathname" ? `/${prefix}` : ""}`, + const redirectUrl = new URL( + `${origin}${type === "pathname" ? `/${prefix}` : ""}/${currentLocale}`, ); - return NextResponse.redirect(url); + return NextResponse.redirect(redirectUrl); } } + // --- Rewrites --- const proxy = req.headers.get("x-proxy"); console.log({ proxy }); @@ -158,36 +205,19 @@ export default auth(async (req) => { expectedHost: `${_page.slug}.stpg.dev`, }); if (_page.customDomain && host !== `${_page.slug}.stpg.dev`) { - if (pathnames.length > 2 && !subdomain) { - const pathname = pathnames.slice(2).join("/"); - const rewriteUrl = new URL(`/${_page.slug}/${pathname}`, req.url); - rewriteUrl.search = url.search; - return NextResponse.rewrite(rewriteUrl); - } - if (_page.customDomain && subdomain) { + // Custom domain: prepend slug to the path + // url.pathname already contains /{locale}/... so this becomes /{slug}/{locale}/... + if (subdomain) { console.log({ url: req.url }); - // const vercelURL = process.env.VERCEL_URL || "www.stpg.dev"; - // console.log({newUrl: vercelURL}) - if (pathnames.length > 2) { - const pathname = pathnames.slice(1).join("/"); - - const rewriteUrl = new URL( - `${pathname}`, - `https://${_page.slug}.stpg.dev`, - ); - console.log({ rewriteUrl }); - rewriteUrl.search = url.search; - return NextResponse.rewrite(rewriteUrl); - } const rewriteUrl = new URL( - `${url.pathname}`, + url.pathname, `https://${_page.slug}.stpg.dev`, ); console.log({ rewriteUrl }); rewriteUrl.search = url.search; return NextResponse.rewrite(rewriteUrl); } - const rewriteUrl = new URL(`/${_page.slug}`, req.url); + const rewriteUrl = new URL(`/${_page.slug}${url.pathname}`, req.url); console.log({ rewriteUrl }); rewriteUrl.search = url.search; return NextResponse.rewrite(rewriteUrl); From c1199e16ee846750c16280d4a523101c055806d7 Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Thu, 12 Mar 2026 21:37:54 +0530 Subject: [PATCH 14/24] ci: apply automated fixes Signed-off-by: Moulik Aggarwal --- apps/status-page/src/app/(public)/client.tsx | 3 ++- apps/status-page/src/components/language-switcher.tsx | 4 +++- .../status-page/static/status-banner-static.tsx | 5 +---- .../status-page/static/status-events-static.tsx | 10 ++-------- .../status-page/static/status-tracker-static.tsx | 6 ++---- apps/status-page/src/i18n/config.ts | 5 +---- 6 files changed, 11 insertions(+), 22 deletions(-) diff --git a/apps/status-page/src/app/(public)/client.tsx b/apps/status-page/src/app/(public)/client.tsx index ba5eafd9b4..5994821d54 100644 --- a/apps/status-page/src/app/(public)/client.tsx +++ b/apps/status-page/src/app/(public)/client.tsx @@ -10,6 +10,8 @@ import { SectionTitle, } from "@/components/content/section"; import { recomputeStyles } from "@/components/status-page/floating-button"; +import { StatusBannerStatic } from "@/components/status-page/static/status-banner-static"; +import { StatusMonitorStatic } from "@/components/status-page/static/status-monitor-static"; import { Status, StatusContent, @@ -17,7 +19,6 @@ import { StatusHeader, StatusTitle, } from "@/components/status-page/status"; -import { StatusBannerStatic } from "@/components/status-page/static/status-banner-static"; import { ThemePalettePicker } from "@/components/themes/theme-palette-picker"; import { ThemeSelect } from "@/components/themes/theme-select"; import { monitors } from "@/data/monitors"; diff --git a/apps/status-page/src/components/language-switcher.tsx b/apps/status-page/src/components/language-switcher.tsx index cb16588f5c..2b2a1efd59 100644 --- a/apps/status-page/src/components/language-switcher.tsx +++ b/apps/status-page/src/components/language-switcher.tsx @@ -38,7 +38,9 @@ export function LanguageSwitcher() { const domainIndex = segments.indexOf(domain); const potentialLocale = segments[domainIndex + 1]; - const hasLocaleSegment = (locales as readonly string[]).includes(potentialLocale); + const hasLocaleSegment = (locales as readonly string[]).includes( + potentialLocale, + ); if (nextLocale === "en") { if (hasLocaleSegment) { diff --git a/apps/status-page/src/components/status-page/static/status-banner-static.tsx b/apps/status-page/src/components/status-page/static/status-banner-static.tsx index cad01aad79..2cc8eded22 100644 --- a/apps/status-page/src/components/status-page/static/status-banner-static.tsx +++ b/apps/status-page/src/components/status-page/static/status-banner-static.tsx @@ -1,11 +1,8 @@ "use client"; import { cn } from "@openstatus/ui/lib/utils"; -import { - StatusBannerContainer, - StatusBannerIcon, -} from "../status-banner"; import { StatusTimestamp } from "../status"; +import { StatusBannerContainer, StatusBannerIcon } from "../status-banner"; export function StatusBannerStatic({ className, diff --git a/apps/status-page/src/components/status-page/static/status-events-static.tsx b/apps/status-page/src/components/status-page/static/status-events-static.tsx index 034d6376f5..956380728f 100644 --- a/apps/status-page/src/components/status-page/static/status-events-static.tsx +++ b/apps/status-page/src/components/status-page/static/status-events-static.tsx @@ -59,10 +59,7 @@ export function StatusEventTimelineReportStatic({ } } else { const lastUpdateDate = new Date(displayedUpdates[index - 1].date); - const timeFromLast = formatDistanceStrict( - updateDate, - lastUpdateDate, - ); + const timeFromLast = formatDistanceStrict(updateDate, lastUpdateDate); durationText = `(${timeFromLast} earlier)`; } @@ -89,10 +86,7 @@ export function StatusEventTimelineReportStatic({ {statusLabels[update.status]}{" "} ·{" "} - + {formatDateTime(update.date)} {" "} diff --git a/apps/status-page/src/components/status-page/static/status-tracker-static.tsx b/apps/status-page/src/components/status-page/static/status-tracker-static.tsx index 6ea54a0c48..cb25a9cdca 100644 --- a/apps/status-page/src/components/status-page/static/status-tracker-static.tsx +++ b/apps/status-page/src/components/status-page/static/status-tracker-static.tsx @@ -116,8 +116,7 @@ export function StatusTrackerStatic({ data }: { data: UptimeData }) { if (prevMonitor) { const prevTracker = prevMonitor.querySelector('[role="toolbar"]'); if (prevTracker) { - const buttons = - prevTracker.querySelectorAll('[role="button"]'); + const buttons = prevTracker.querySelectorAll('[role="button"]'); const button = buttons?.[focusedIndex] as HTMLElement; if (button) { button.focus(); @@ -135,8 +134,7 @@ export function StatusTrackerStatic({ data }: { data: UptimeData }) { if (nextMonitor) { const nextTracker = nextMonitor.querySelector('[role="toolbar"]'); if (nextTracker) { - const buttons = - nextTracker.querySelectorAll('[role="button"]'); + const buttons = nextTracker.querySelectorAll('[role="button"]'); const button = buttons?.[focusedIndex] as HTMLElement; if (button) { button.focus(); diff --git a/apps/status-page/src/i18n/config.ts b/apps/status-page/src/i18n/config.ts index 4b89929000..ad12ac8044 100644 --- a/apps/status-page/src/i18n/config.ts +++ b/apps/status-page/src/i18n/config.ts @@ -1,6 +1,3 @@ -export const locales = [ - "en", - "fr" -] as const; +export const locales = ["en", "fr"] as const; export type Locale = (typeof locales)[number]; export const defaultLocale: Locale = "en"; From 8e0d807facd2deea448d146a05487c0c27e44b95 Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Thu, 12 Mar 2026 08:06:27 +0530 Subject: [PATCH 15/24] Removed unncessary static component with ui package Signed-off-by: Moulik Aggarwal --- apps/status-page/messages/de.json | 2 +- apps/status-page/messages/en.json | 2 +- apps/status-page/messages/es.json | 2 +- apps/status-page/messages/fr.json | 2 +- apps/status-page/messages/ja.json | 2 +- apps/status-page/messages/ko.json | 2 +- apps/status-page/messages/pt.json | 2 +- apps/status-page/messages/zh.json | 2 +- apps/status-page/src/app/(public)/client.tsx | 4 +- .../static/status-banner-static.tsx | 45 -- .../static/status-events-static.tsx | 114 ----- .../static/status-monitor-static.tsx | 12 +- .../static/status-tracker-static.tsx | 413 ------------------ 13 files changed, 15 insertions(+), 589 deletions(-) delete mode 100644 apps/status-page/src/components/status-page/static/status-banner-static.tsx delete mode 100644 apps/status-page/src/components/status-page/static/status-events-static.tsx delete mode 100644 apps/status-page/src/components/status-page/static/status-tracker-static.tsx diff --git a/apps/status-page/messages/de.json b/apps/status-page/messages/de.json index f0743b8f21..8a4e411142 100644 --- a/apps/status-page/messages/de.json +++ b/apps/status-page/messages/de.json @@ -1,8 +1,8 @@ { "Y9HHck": "", "1QcGkA": "", - "wSZR47": "", "txkW56": "", + "wSZR47": "", "OrFVks": "", "n36zhX": "", "qIAQSi": "", diff --git a/apps/status-page/messages/en.json b/apps/status-page/messages/en.json index c22b195125..aefe519118 100644 --- a/apps/status-page/messages/en.json +++ b/apps/status-page/messages/en.json @@ -1,8 +1,8 @@ { "Y9HHck": "Authenticate", "1QcGkA": "Enter your email to receive a magic link for accessing the status page. Note: Only emails from approved domains are accepted.", - "wSZR47": "Submit", "txkW56": "Submitting...", + "wSZR47": "Submit", "OrFVks": "Check your inbox!", "n36zhX": "Access the status page by clicking the link in the email.", "qIAQSi": "Protected Page", diff --git a/apps/status-page/messages/es.json b/apps/status-page/messages/es.json index f0743b8f21..8a4e411142 100644 --- a/apps/status-page/messages/es.json +++ b/apps/status-page/messages/es.json @@ -1,8 +1,8 @@ { "Y9HHck": "", "1QcGkA": "", - "wSZR47": "", "txkW56": "", + "wSZR47": "", "OrFVks": "", "n36zhX": "", "qIAQSi": "", diff --git a/apps/status-page/messages/fr.json b/apps/status-page/messages/fr.json index f0743b8f21..8a4e411142 100644 --- a/apps/status-page/messages/fr.json +++ b/apps/status-page/messages/fr.json @@ -1,8 +1,8 @@ { "Y9HHck": "", "1QcGkA": "", - "wSZR47": "", "txkW56": "", + "wSZR47": "", "OrFVks": "", "n36zhX": "", "qIAQSi": "", diff --git a/apps/status-page/messages/ja.json b/apps/status-page/messages/ja.json index f0743b8f21..8a4e411142 100644 --- a/apps/status-page/messages/ja.json +++ b/apps/status-page/messages/ja.json @@ -1,8 +1,8 @@ { "Y9HHck": "", "1QcGkA": "", - "wSZR47": "", "txkW56": "", + "wSZR47": "", "OrFVks": "", "n36zhX": "", "qIAQSi": "", diff --git a/apps/status-page/messages/ko.json b/apps/status-page/messages/ko.json index f0743b8f21..8a4e411142 100644 --- a/apps/status-page/messages/ko.json +++ b/apps/status-page/messages/ko.json @@ -1,8 +1,8 @@ { "Y9HHck": "", "1QcGkA": "", - "wSZR47": "", "txkW56": "", + "wSZR47": "", "OrFVks": "", "n36zhX": "", "qIAQSi": "", diff --git a/apps/status-page/messages/pt.json b/apps/status-page/messages/pt.json index f0743b8f21..8a4e411142 100644 --- a/apps/status-page/messages/pt.json +++ b/apps/status-page/messages/pt.json @@ -1,8 +1,8 @@ { "Y9HHck": "", "1QcGkA": "", - "wSZR47": "", "txkW56": "", + "wSZR47": "", "OrFVks": "", "n36zhX": "", "qIAQSi": "", diff --git a/apps/status-page/messages/zh.json b/apps/status-page/messages/zh.json index f0743b8f21..8a4e411142 100644 --- a/apps/status-page/messages/zh.json +++ b/apps/status-page/messages/zh.json @@ -1,8 +1,8 @@ { "Y9HHck": "", "1QcGkA": "", - "wSZR47": "", "txkW56": "", + "wSZR47": "", "OrFVks": "", "n36zhX": "", "qIAQSi": "", diff --git a/apps/status-page/src/app/(public)/client.tsx b/apps/status-page/src/app/(public)/client.tsx index 5994821d54..175b55fdf5 100644 --- a/apps/status-page/src/app/(public)/client.tsx +++ b/apps/status-page/src/app/(public)/client.tsx @@ -10,7 +10,6 @@ import { SectionTitle, } from "@/components/content/section"; import { recomputeStyles } from "@/components/status-page/floating-button"; -import { StatusBannerStatic } from "@/components/status-page/static/status-banner-static"; import { StatusMonitorStatic } from "@/components/status-page/static/status-monitor-static"; import { Status, @@ -24,6 +23,7 @@ import { ThemeSelect } from "@/components/themes/theme-select"; import { monitors } from "@/data/monitors"; import { useTRPC } from "@/lib/trpc/client"; import { THEMES, THEME_KEYS } from "@openstatus/theme-store"; +import { StatusBanner } from "@openstatus/ui/components/blocks/status-banner"; import { Button } from "@openstatus/ui/components/ui/button"; import { Input } from "@openstatus/ui/components/ui/input"; import { Separator } from "@openstatus/ui/components/ui/separator"; @@ -295,7 +295,7 @@ function ThemePlaygroundStatus({ Get informed about our services. - + {/* TODO: create mock data */} & { - status?: "success" | "degraded" | "error" | "info"; -}) { - return ( - - -
-
- - All Systems Operational - - - Degraded Performance - - - Downtime Performance - - - Maintenance - -
- -
-
- ); -} diff --git a/apps/status-page/src/components/status-page/static/status-events-static.tsx b/apps/status-page/src/components/status-page/static/status-events-static.tsx deleted file mode 100644 index 956380728f..0000000000 --- a/apps/status-page/src/components/status-page/static/status-events-static.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import { ProcessMessage } from "@/components/content/process-message"; -import { TimestampHoverCard } from "@/components/content/timestamp-hover-card"; -import { formatDateTime } from "@/lib/formatter"; -import { cn } from "@openstatus/ui/lib/utils"; -import { formatDistanceStrict } from "date-fns"; -import { - StatusEventTimelineDot, - StatusEventTimelineMessage, - StatusEventTimelineSeparator, - StatusEventTimelineTitle, -} from "../status-events"; - -export function StatusEventTimelineReportStatic({ - className, - updates, - withDot = true, - maxUpdates, - ...props -}: React.ComponentProps<"div"> & { - reportId: number; - updates: { - date: Date; - message: string; - status: "investigating" | "identified" | "monitoring" | "resolved"; - }[]; - withDot?: boolean; - maxUpdates?: number; -}) { - const sortedUpdates = [...updates].sort( - (a, b) => b.date.getTime() - a.date.getTime(), - ); - const displayedUpdates = maxUpdates - ? sortedUpdates.slice(0, maxUpdates) - : sortedUpdates; - - const statusLabels = { - resolved: "Resolved", - monitoring: "Monitoring", - identified: "Identified", - investigating: "Investigating", - } as const; - - return ( -
- {displayedUpdates.map((update, index) => { - const updateDate = new Date(update.date); - let durationText: string | undefined; - - if (index === 0) { - const startedAt = new Date( - sortedUpdates[sortedUpdates.length - 1].date, - ); - const duration = formatDistanceStrict(startedAt, updateDate); - - if (duration !== "0 seconds" && update.status === "resolved") { - durationText = `(in ${duration})`; - } - } else { - const lastUpdateDate = new Date(displayedUpdates[index - 1].date); - const timeFromLast = formatDistanceStrict(updateDate, lastUpdateDate); - durationText = `(${timeFromLast} earlier)`; - } - - return ( -
-
-
- {withDot ? ( -
-
- -
- {index !== displayedUpdates.length - 1 ? ( - - ) : null} -
- ) : null} -
- - {statusLabels[update.status]}{" "} - ·{" "} - - - {formatDateTime(update.date)} - - {" "} - {durationText ? ( - - {durationText} - - ) : null} - - - {update.message.trim() === "" ? ( - - - ) : ( - - )} - -
-
-
-
- ); - })} -
- ); -} diff --git a/apps/status-page/src/components/status-page/static/status-monitor-static.tsx b/apps/status-page/src/components/status-page/static/status-monitor-static.tsx index 9e5f8a4a0e..f1a3a136b8 100644 --- a/apps/status-page/src/components/status-page/static/status-monitor-static.tsx +++ b/apps/status-page/src/components/status-page/static/status-monitor-static.tsx @@ -1,6 +1,10 @@ "use client"; import type { RouterOutputs } from "@openstatus/api"; +import { + StatusBar, + StatusBarSkeleton, +} from "@openstatus/ui/components/blocks/status-bar"; import { Skeleton } from "@openstatus/ui/components/ui/skeleton"; import { cn } from "@openstatus/ui/lib/utils"; import { formatDistanceToNowStrict } from "date-fns"; @@ -16,8 +20,6 @@ import { StatusMonitorUptime, StatusMonitorUptimeSkeleton, } from "../status-monitor"; -import { StatusTrackerSkeleton } from "../status-tracker"; -import { StatusTrackerStatic } from "./status-tracker-static"; type VariantType = "success" | "degraded" | "error" | "info"; @@ -74,11 +76,7 @@ export function StatusMonitorStatic({ )}
- {isLoading ? ( - - ) : ( - - )} + {isLoading ? : }
); diff --git a/apps/status-page/src/components/status-page/static/status-tracker-static.tsx b/apps/status-page/src/components/status-page/static/status-tracker-static.tsx deleted file mode 100644 index cb25a9cdca..0000000000 --- a/apps/status-page/src/components/status-page/static/status-tracker-static.tsx +++ /dev/null @@ -1,413 +0,0 @@ -"use client"; - -import { Kbd } from "@/components/common/kbd"; -import { formatDateRange } from "@/lib/formatter"; -import type { RouterOutputs } from "@openstatus/api"; -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from "@openstatus/ui/components/ui/hover-card"; -import { Separator } from "@openstatus/ui/components/ui/separator"; -import { useMediaQuery } from "@openstatus/ui/hooks/use-media-query"; -import { cn } from "@openstatus/ui/lib/utils"; -import { formatDistanceStrict } from "date-fns"; -import { useEffect, useRef, useState } from "react"; -import { chartConfig } from "../utils"; - -type UptimeData = NonNullable< - RouterOutputs["statusPage"]["getUptime"] ->[number]["data"]; - -const staticLabels: Record = { - success: "Normal", - degraded: "Degraded", - error: "Error", - info: "Maintenance", - empty: "No Data", -}; - -export function StatusTrackerStatic({ data }: { data: UptimeData }) { - const [pinnedIndex, setPinnedIndex] = useState(null); - const [focusedIndex, setFocusedIndex] = useState(null); - const [hoveredIndex, setHoveredIndex] = useState(null); - const containerRef = useRef(null); - const hoverTimeoutRef = useRef(null); - const isTouch = useMediaQuery("(hover: none)"); - - useEffect(() => { - const handleOutsideClick = (e: MouseEvent) => { - if ( - pinnedIndex !== null && - containerRef.current && - !containerRef.current.contains(e.target as Node) - ) { - setPinnedIndex(null); - } - }; - - if (pinnedIndex !== null) { - document.addEventListener("mousedown", handleOutsideClick); - return () => - document.removeEventListener("mousedown", handleOutsideClick); - } - }, [pinnedIndex]); - - useEffect(() => { - if (focusedIndex !== null && containerRef.current) { - const buttons = containerRef.current.querySelectorAll('[role="button"]'); - const targetButton = buttons[focusedIndex] as HTMLElement; - if (targetButton) { - targetButton.focus(); - } - } - }, [focusedIndex]); - - useEffect(() => { - return () => { - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - } - }; - }, []); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Escape") { - setPinnedIndex(null); - setFocusedIndex(null); - setHoveredIndex(null); - - if (focusedIndex !== null) { - const buttons = - containerRef.current?.querySelectorAll('[role="button"]'); - const button = buttons?.[focusedIndex] as HTMLElement; - if (button) { - button.blur(); - } - } - - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - hoverTimeoutRef.current = null; - } - return; - } - - if (focusedIndex !== null) { - switch (e.key) { - case "ArrowLeft": - e.preventDefault(); - setFocusedIndex((prev) => - prev !== null && prev > 0 ? prev - 1 : data.length - 1, - ); - break; - case "ArrowRight": - e.preventDefault(); - setFocusedIndex((prev) => - prev !== null && prev < data.length - 1 ? prev + 1 : 0, - ); - break; - case "ArrowUp": - e.preventDefault(); - { - const prevMonitor = containerRef.current?.closest( - '[data-slot="status-monitor"]', - )?.previousElementSibling; - if (prevMonitor) { - const prevTracker = prevMonitor.querySelector('[role="toolbar"]'); - if (prevTracker) { - const buttons = prevTracker.querySelectorAll('[role="button"]'); - const button = buttons?.[focusedIndex] as HTMLElement; - if (button) { - button.focus(); - } - } - } - } - break; - case "ArrowDown": - e.preventDefault(); - { - const nextMonitor = containerRef.current?.closest( - '[data-slot="status-monitor"]', - )?.nextElementSibling; - if (nextMonitor) { - const nextTracker = nextMonitor.querySelector('[role="toolbar"]'); - if (nextTracker) { - const buttons = nextTracker.querySelectorAll('[role="button"]'); - const button = buttons?.[focusedIndex] as HTMLElement; - if (button) { - button.focus(); - } - } - } - } - break; - case "Enter": - case "Escape": - case " ": - e.preventDefault(); - handleBarClick(focusedIndex); - break; - } - } - }; - - const handleBarClick = (index: number) => { - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - hoverTimeoutRef.current = null; - } - if (pinnedIndex === index) { - setPinnedIndex(null); - } else { - setPinnedIndex(index); - } - }; - - const handleBarFocus = (index: number) => { - setFocusedIndex(index); - }; - - const handleBarBlur = (e: React.FocusEvent, _currentIndex: number) => { - const relatedTarget = e.relatedTarget as HTMLElement; - const isMovingToAnotherBar = - relatedTarget && - relatedTarget.closest('[role="toolbar"]') === containerRef.current && - relatedTarget.getAttribute("role") === "button"; - - if (!isMovingToAnotherBar) { - setFocusedIndex(null); - } - }; - - const handleBarMouseEnter = (index: number) => { - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - hoverTimeoutRef.current = null; - } - setHoveredIndex(index); - }; - - const handleBarMouseLeave = () => { - hoverTimeoutRef.current = setTimeout(() => { - setHoveredIndex(null); - }, 100); - }; - - const handleHoverCardMouseEnter = () => { - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - hoverTimeoutRef.current = null; - } - }; - - const handleHoverCardMouseLeave = () => { - setHoveredIndex(null); - }; - - return ( -
- {data.map((item, index) => { - const isPinned = pinnedIndex === index; - const isFocused = focusedIndex === index; - const isHovered = hoveredIndex === index; - - return ( - - -
handleBarClick(index)} - onFocus={() => handleBarFocus(index)} - onBlur={(e) => handleBarBlur(e, index)} - onMouseEnter={() => handleBarMouseEnter(index)} - onMouseLeave={handleBarMouseLeave} - tabIndex={ - index === data.length - 1 && focusedIndex === null - ? 0 - : isFocused - ? 0 - : -1 - } - role="button" - aria-label={`Day ${index + 1} status`} - aria-pressed={isPinned} - > - {item.bar.map((segment, segmentIndex) => ( -
- ))} -
- - -
-
- {new Date(item.day).toLocaleDateString("default", { - day: "numeric", - month: "short", - year: "numeric", - })} -
- -
- {item.card.map((cardItem, cardIndex) => ( - - ))} -
- {item.events.length > 0 && ( - <> - -
- {item.events.map((event) => { - const eventStatus = - event.type === "incident" - ? "error" - : event.type === "report" - ? "degraded" - : "info"; - - return ( - - ); - })} -
- - )} - {isPinned && !isTouch && ( - <> - -
- Click again to unpin - Esc -
- - )} -
-
- - ); - })} -
- ); -} - -function StatusTrackerContentStatic({ - status, - value, -}: { - status: "success" | "degraded" | "error" | "info" | "empty"; - value: string; -}) { - return ( -
-
-
-
{staticLabels[status]}
-
-
- {value} -
-
- ); -} - -function StatusTrackerEventStatic({ - name, - from, - to, - status, -}: { - name: string; - from?: Date | null; - to?: Date | null; - status: "success" | "degraded" | "error" | "info" | "empty"; -}) { - if (!from) return null; - - return ( -
-
-
-
-
-
{name}
-
-
-
- {formatDateRange(from, to ?? undefined)}{" "} - - {formatDuration({ from, to, name, status })} - -
-
- ); -} - -const formatDuration = ({ - from, - to, - name, -}: { - name: string; - from?: Date | null; - to?: Date | null; - status: "success" | "degraded" | "error" | "info" | "empty"; -}) => { - if (!from) return null; - if (!to) return "ongoing"; - const duration = formatDistanceStrict(from, to); - const isMultipleIncidents = name.includes("Downtime ("); - if (isMultipleIncidents) return `across ${duration}`; - if (duration === "0 seconds") return null; - return duration; -}; From 792216b2e432545a7c965430d6b0447f77764786 Mon Sep 17 00:00:00 2001 From: Moulik Aggarwal Date: Thu, 12 Mar 2026 08:22:29 +0530 Subject: [PATCH 16/24] Updated status-events with blocks component Signed-off-by: Moulik Aggarwal --- apps/status-page/messages/de.json | 1 - apps/status-page/messages/en.json | 1 - apps/status-page/messages/es.json | 1 - apps/status-page/messages/fr.json | 1 - apps/status-page/messages/ja.json | 1 - apps/status-page/messages/ko.json | 1 - apps/status-page/messages/pt.json | 1 - apps/status-page/messages/zh.json | 1 - .../components/status-page/status-events.tsx | 81 ++----------------- 9 files changed, 6 insertions(+), 83 deletions(-) diff --git a/apps/status-page/messages/de.json b/apps/status-page/messages/de.json index 8a4e411142..91c6836b11 100644 --- a/apps/status-page/messages/de.json +++ b/apps/status-page/messages/de.json @@ -59,7 +59,6 @@ "1P6GMj": "", "7cv4Uf": "", "/GKH/w": "", - "2K7gg0": "", "FDReLp": "", "qDj0JR": "", "CYs0LF": "", diff --git a/apps/status-page/messages/en.json b/apps/status-page/messages/en.json index aefe519118..5a5d5c0ed2 100644 --- a/apps/status-page/messages/en.json +++ b/apps/status-page/messages/en.json @@ -59,7 +59,6 @@ "1P6GMj": "Monitoring", "7cv4Uf": "Identified", "/GKH/w": "Investigating", - "2K7gg0": "View full report", "FDReLp": "No recent notifications", "qDj0JR": "There have been no reports within the last 7 days.", "CYs0LF": "View events history", diff --git a/apps/status-page/messages/es.json b/apps/status-page/messages/es.json index 8a4e411142..91c6836b11 100644 --- a/apps/status-page/messages/es.json +++ b/apps/status-page/messages/es.json @@ -59,7 +59,6 @@ "1P6GMj": "", "7cv4Uf": "", "/GKH/w": "", - "2K7gg0": "", "FDReLp": "", "qDj0JR": "", "CYs0LF": "", diff --git a/apps/status-page/messages/fr.json b/apps/status-page/messages/fr.json index 8a4e411142..91c6836b11 100644 --- a/apps/status-page/messages/fr.json +++ b/apps/status-page/messages/fr.json @@ -59,7 +59,6 @@ "1P6GMj": "", "7cv4Uf": "", "/GKH/w": "", - "2K7gg0": "", "FDReLp": "", "qDj0JR": "", "CYs0LF": "", diff --git a/apps/status-page/messages/ja.json b/apps/status-page/messages/ja.json index 8a4e411142..91c6836b11 100644 --- a/apps/status-page/messages/ja.json +++ b/apps/status-page/messages/ja.json @@ -59,7 +59,6 @@ "1P6GMj": "", "7cv4Uf": "", "/GKH/w": "", - "2K7gg0": "", "FDReLp": "", "qDj0JR": "", "CYs0LF": "", diff --git a/apps/status-page/messages/ko.json b/apps/status-page/messages/ko.json index 8a4e411142..91c6836b11 100644 --- a/apps/status-page/messages/ko.json +++ b/apps/status-page/messages/ko.json @@ -59,7 +59,6 @@ "1P6GMj": "", "7cv4Uf": "", "/GKH/w": "", - "2K7gg0": "", "FDReLp": "", "qDj0JR": "", "CYs0LF": "", diff --git a/apps/status-page/messages/pt.json b/apps/status-page/messages/pt.json index 8a4e411142..91c6836b11 100644 --- a/apps/status-page/messages/pt.json +++ b/apps/status-page/messages/pt.json @@ -59,7 +59,6 @@ "1P6GMj": "", "7cv4Uf": "", "/GKH/w": "", - "2K7gg0": "", "FDReLp": "", "qDj0JR": "", "CYs0LF": "", diff --git a/apps/status-page/messages/zh.json b/apps/status-page/messages/zh.json index 8a4e411142..91c6836b11 100644 --- a/apps/status-page/messages/zh.json +++ b/apps/status-page/messages/zh.json @@ -59,7 +59,6 @@ "1P6GMj": "", "7cv4Uf": "", "/GKH/w": "", - "2K7gg0": "", "FDReLp": "", "qDj0JR": "", "CYs0LF": "", diff --git a/apps/status-page/src/components/status-page/status-events.tsx b/apps/status-page/src/components/status-page/status-events.tsx index 8eb2ce174e..485deb93ba 100644 --- a/apps/status-page/src/components/status-page/status-events.tsx +++ b/apps/status-page/src/components/status-page/status-events.tsx @@ -1,8 +1,13 @@ import { ProcessMessage } from "@/components/content/process-message"; import { TimestampHoverCard } from "@/components/content/timestamp-hover-card"; import { formatDate, formatDateRange, formatDateTime } from "@/lib/formatter"; +import { + StatusEventTimelineDot, + StatusEventTimelineMessage, + StatusEventTimelineSeparator, + StatusEventTimelineTitle, +} from "@openstatus/ui/components/blocks/status-events"; import { Badge } from "@openstatus/ui/components/ui/badge"; -import { Separator } from "@openstatus/ui/components/ui/separator"; import { Tooltip, TooltipContent, @@ -357,77 +362,3 @@ export function StatusEventTimelineMaintenance({
); } - -export function StatusEventTimelineTitle({ - className, - children, - ...props -}: React.ComponentProps<"div">) { - return ( -
- {children} -
- ); -} - -export function StatusEventTimelineMessage({ - className, - children, - ...props -}: React.ComponentProps<"div">) { - return ( -
- {children} -
- ); -} - -export function StatusEventTimelineDot({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ); -} - -export function StatusEventTimelineSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} From 6ded7165bb7f070041b609fa8c9ff7f9ceb3f099 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Fri, 20 Mar 2026 17:51:06 +0100 Subject: [PATCH 17/24] wip: i18n --- apps/status-page/messages/de.json | 174 +++++---- apps/status-page/messages/en.json | 4 +- apps/status-page/messages/es.json | 90 ----- apps/status-page/messages/fr.json | 174 +++++---- apps/status-page/messages/ja.json | 90 ----- apps/status-page/messages/ko.json | 90 ----- apps/status-page/messages/pt.json | 90 ----- apps/status-page/messages/zh.json | 90 ----- apps/status-page/next.config.ts | 69 +--- .../content/timestamp-hover-card.tsx | 2 + .../src/components/language-switcher.tsx | 84 ---- .../src/components/locale-switcher.tsx | 93 +++++ .../status-page/src/components/nav/footer.tsx | 6 +- .../status-page/src/components/nav/header.tsx | 17 +- .../src/hooks/use-pathname-prefix.ts | 32 +- apps/status-page/src/i18n/config.ts | 25 +- apps/status-page/src/i18n/routing.ts | 2 +- .../src/lib/resolve-pathname-prefix.test.ts | 195 ++++++++++ .../src/lib/resolve-pathname-prefix.ts | 41 ++ .../status-page/src/lib/resolve-route.test.ts | 364 ++++++++++++++++++ apps/status-page/src/lib/resolve-route.ts | 103 +++++ apps/status-page/src/proxy.ts | 151 +++----- packages/db/src/seed.mts | 15 + 23 files changed, 1091 insertions(+), 910 deletions(-) delete mode 100644 apps/status-page/messages/es.json delete mode 100644 apps/status-page/messages/ja.json delete mode 100644 apps/status-page/messages/ko.json delete mode 100644 apps/status-page/messages/pt.json delete mode 100644 apps/status-page/messages/zh.json delete mode 100644 apps/status-page/src/components/language-switcher.tsx create mode 100644 apps/status-page/src/components/locale-switcher.tsx create mode 100644 apps/status-page/src/lib/resolve-pathname-prefix.test.ts create mode 100644 apps/status-page/src/lib/resolve-pathname-prefix.ts create mode 100644 apps/status-page/src/lib/resolve-route.test.ts create mode 100644 apps/status-page/src/lib/resolve-route.ts diff --git a/apps/status-page/messages/de.json b/apps/status-page/messages/de.json index 91c6836b11..b9d7b2a166 100644 --- a/apps/status-page/messages/de.json +++ b/apps/status-page/messages/de.json @@ -1,90 +1,88 @@ { - "Y9HHck": "", - "1QcGkA": "", - "txkW56": "", - "wSZR47": "", - "OrFVks": "", - "n36zhX": "", - "qIAQSi": "", - "lbw10C": "", - "t262xH": "", - "DwevKz": "", - "Ppx673": "", - "JCMXwP": "", - "I7B7SH": "", - "OSI607": "", - "a9S/OH": "", - "HSv9BP": "", - "VL1Y/1": "", - "Ew1f8q": "", - "awr0AJ": "", - "CVsoUM": "", - "BRGcS0": "", - "9vqdq3": "", - "fFOayY": "", - "u81G9+": "", - "i2FBWn": "", - "G5Lt80": "", - "YV7rXP": "", - "6zzIEm": "", - "6pCzRs": "", - "zL23+z": "", - "79eRW1": "", - "dX7+Rv": "", - "m0fapd": "", - "sy+pv5": "", - "9Utk00": "", - "Eq5gCU": "", - "qp+wDV": "", - "d/jCcy": "", - "FlVuUh": "", - "8aUjqQ": "", - "5sg7KC": "", - "IGY48m": "", - "Pgb3Xj": "", - "WOH7Yj": "", - "L7z2/k": "", - "NOyDVq": "", - "tzMNF3": "", - "ZvKSfJ": "", - "xJrRMG": "", - "tKMlOc": "", - "krEziQ": "", - "BQBZU+": "", - "b9fOA1": "", - "80EXUh": "", - "dudqv/": "", - "2syGZB": "", - "W6nSYE": "", - "1P6GMj": "", - "7cv4Uf": "", - "/GKH/w": "", - "FDReLp": "", - "qDj0JR": "", - "CYs0LF": "", - "kkpP2k": "", - "Dnob31": "", - "VQDmmK": "", - "JOZGPR": "", - "GbVCQb": "", - "myq2ZL": "", - "KN7zKn": "", - "D3rOMr": "", - "uPb/gh": "", - "sjzDbu": "", - "q0qMyV": "", - "9y9QQh": "", - "waUHa4": "", - "cVqFq/": "", - "gczcC5": "", - "8OoV56": "", - "Auj/Ki": "", - "SyYroX": "", - "PSqtlY": "", - "rptmhC": "", - "2yCGR2": "", - "u5aHb4": "", - "p556q3": "", - "4l6vz1": "", - "45YlLU": "" + "Y9HHck": "Authentifizieren", + "1QcGkA": "Geben Sie Ihre E-Mail-Adresse ein, um einen Magic Link für den Zugriff auf die Statusseite zu erhalten. Hinweis: Es werden nur E-Mails von genehmigten Domains akzeptiert.", + "wSZR47": "Absenden", + "txkW56": "Wird gesendet...", + "OrFVks": "Überprüfen Sie Ihren Posteingang!", + "n36zhX": "Greifen Sie auf die Statusseite zu, indem Sie auf den Link in der E-Mail klicken.", + "qIAQSi": "Geschützte Seite", + "lbw10C": "Geben Sie das Passwort ein, um auf die Statusseite zuzugreifen.", + "t262xH": "E-Mail und Weiterleitungs-URL sind erforderlich", + "DwevKz": "Bei der Anmeldung ist ein unerwarteter Fehler aufgetreten", + "Ppx673": "Berichte", + "JCMXwP": "Wartungen", + "I7B7SH": "Keine Wartungen gefunden", + "OSI607": "Keine Wartungen für diese Statusseite gefunden.", + "a9S/OH": "Wartung nicht gefunden", + "HSv9BP": "Die gesuchte Wartung existiert nicht.", + "VL1Y/1": "Bericht nicht gefunden", + "Ew1f8q": "Der gesuchte Bericht existiert nicht.", + "awr0AJ": "Monitor nicht gefunden", + "CVsoUM": "Der gesuchte Monitor existiert nicht.", + "BRGcS0": "Globale Latenz", + "9vqdq3": "Regionale Latenz", + "fFOayY": "Regionen", + "u81G9+": "Verfügbarkeit", + "i2FBWn": "Prüfungen", + "G5Lt80": "Die aggregierte Latenz aller aktiven Regionen basierend auf verschiedenen Quantilen.", + "YV7rXP": "Latenz nach Region", + "6zzIEm": "Regionale Latenz pro p75-Quantil, sortiert nach langsamster Region. Vergleichen Sie bis zu 6 Regionen.", + "6pCzRs": "Gesamtverfügbarkeit", + "zL23+z": "Hauptwerte der Verfügbarkeit, transparent dargestellt.", + "79eRW1": "Bestätigung läuft...", + "dX7+Rv": "Bestätigt", + "m0fapd": "Bestätigung fehlgeschlagen", + "sy+pv5": "E-Mail", + "9Utk00": "Abonnement wird aktualisiert...", + "Eq5gCU": "Abonnement aktualisiert", + "qp+wDV": "Aktualisierung des Abonnements fehlgeschlagen", + "d/jCcy": "Bestimmte Komponenten abonnieren", + "FlVuUh": "Keine Komponenten zum Abonnieren", + "8aUjqQ": "Diese Statusseite hat keine Komponenten zum Abonnieren.", + "5sg7KC": "Passwort", + "IGY48m": "Wird abonniert...", + "Pgb3Xj": "Abonniert", + "WOH7Yj": "Abonnement fehlgeschlagen", + "L7z2/k": "Diese Seite hat keine Komponenten zum Abonnieren.", + "NOyDVq": "betrieben von", + "tzMNF3": "Status", + "ZvKSfJ": "Ereignisse", + "xJrRMG": "Monitore", + "tKMlOc": "Menü", + "krEziQ": "Kontakt aufnehmen", + "BQBZU+": "Alle Systeme betriebsbereit", + "b9fOA1": "Eingeschränkte Leistung", + "80EXUh": "Ausfall", + "dudqv/": "Wartung", + "2syGZB": "Bericht gelöst", + "W6nSYE": "Gelöst", + "1P6GMj": "Überwachung", + "7cv4Uf": "Identifiziert", + "/GKH/w": "Wird untersucht", + "FDReLp": "Keine aktuellen Benachrichtigungen", + "qDj0JR": "In den letzten 7 Tagen gab es keine Berichte.", + "CYs0LF": "Ereignisverlauf anzeigen", + "kkpP2k": "heute", + "Dnob31": "Betriebsbereit", + "VQDmmK": "Eingeschränkt", + "JOZGPR": "Ausfall", + "GbVCQb": "Erneut klicken zum Lösen", + "myq2ZL": "Normal", + "KN7zKn": "Fehler", + "D3rOMr": "Keine Daten", + "uPb/gh": "Updates erhalten", + "sjzDbu": "Slack", + "q0qMyV": "RSS", + "9y9QQh": "JSON", + "waUHa4": "SSH", + "cVqFq/": "Erhalten Sie E-Mail-Benachrichtigungen, wenn ein Bericht erstellt oder gelöst wird", + "gczcC5": "Abonnieren", + "8OoV56": "RSS-Feed abrufen", + "Auj/Ki": "Atom-Feed abrufen", + "SyYroX": "JSON-Updates abrufen", + "PSqtlY": "Status über SSH abrufen", + "rptmhC": "Für Status-Updates in Slack fügen Sie den folgenden Text in einen beliebigen Kanal ein.", + "2yCGR2": "Link in die Zwischenablage kopiert", + "u5aHb4": "Link kopieren", + "45YlLU": "Bestätigen Sie Ihre E-Mail-Adresse, um Updates zu erhalten, und schon sind Sie fertig." } diff --git a/apps/status-page/messages/en.json b/apps/status-page/messages/en.json index 5a5d5c0ed2..563ebb6972 100644 --- a/apps/status-page/messages/en.json +++ b/apps/status-page/messages/en.json @@ -1,8 +1,8 @@ { "Y9HHck": "Authenticate", "1QcGkA": "Enter your email to receive a magic link for accessing the status page. Note: Only emails from approved domains are accepted.", - "txkW56": "Submitting...", "wSZR47": "Submit", + "txkW56": "Submitting...", "OrFVks": "Check your inbox!", "n36zhX": "Access the status page by clicking the link in the email.", "qIAQSi": "Protected Page", @@ -84,7 +84,5 @@ "rptmhC": "For status updates in Slack, paste the text below into any channel.", "2yCGR2": "Link copied to clipboard", "u5aHb4": "Copy Link", - "p556q3": "Copied", - "4l6vz1": "Copy", "45YlLU": "Validate your email to receive updates and you are all set." } diff --git a/apps/status-page/messages/es.json b/apps/status-page/messages/es.json deleted file mode 100644 index 91c6836b11..0000000000 --- a/apps/status-page/messages/es.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "Y9HHck": "", - "1QcGkA": "", - "txkW56": "", - "wSZR47": "", - "OrFVks": "", - "n36zhX": "", - "qIAQSi": "", - "lbw10C": "", - "t262xH": "", - "DwevKz": "", - "Ppx673": "", - "JCMXwP": "", - "I7B7SH": "", - "OSI607": "", - "a9S/OH": "", - "HSv9BP": "", - "VL1Y/1": "", - "Ew1f8q": "", - "awr0AJ": "", - "CVsoUM": "", - "BRGcS0": "", - "9vqdq3": "", - "fFOayY": "", - "u81G9+": "", - "i2FBWn": "", - "G5Lt80": "", - "YV7rXP": "", - "6zzIEm": "", - "6pCzRs": "", - "zL23+z": "", - "79eRW1": "", - "dX7+Rv": "", - "m0fapd": "", - "sy+pv5": "", - "9Utk00": "", - "Eq5gCU": "", - "qp+wDV": "", - "d/jCcy": "", - "FlVuUh": "", - "8aUjqQ": "", - "5sg7KC": "", - "IGY48m": "", - "Pgb3Xj": "", - "WOH7Yj": "", - "L7z2/k": "", - "NOyDVq": "", - "tzMNF3": "", - "ZvKSfJ": "", - "xJrRMG": "", - "tKMlOc": "", - "krEziQ": "", - "BQBZU+": "", - "b9fOA1": "", - "80EXUh": "", - "dudqv/": "", - "2syGZB": "", - "W6nSYE": "", - "1P6GMj": "", - "7cv4Uf": "", - "/GKH/w": "", - "FDReLp": "", - "qDj0JR": "", - "CYs0LF": "", - "kkpP2k": "", - "Dnob31": "", - "VQDmmK": "", - "JOZGPR": "", - "GbVCQb": "", - "myq2ZL": "", - "KN7zKn": "", - "D3rOMr": "", - "uPb/gh": "", - "sjzDbu": "", - "q0qMyV": "", - "9y9QQh": "", - "waUHa4": "", - "cVqFq/": "", - "gczcC5": "", - "8OoV56": "", - "Auj/Ki": "", - "SyYroX": "", - "PSqtlY": "", - "rptmhC": "", - "2yCGR2": "", - "u5aHb4": "", - "p556q3": "", - "4l6vz1": "", - "45YlLU": "" -} diff --git a/apps/status-page/messages/fr.json b/apps/status-page/messages/fr.json index 91c6836b11..23141c4e56 100644 --- a/apps/status-page/messages/fr.json +++ b/apps/status-page/messages/fr.json @@ -1,90 +1,88 @@ { - "Y9HHck": "", - "1QcGkA": "", - "txkW56": "", - "wSZR47": "", - "OrFVks": "", - "n36zhX": "", - "qIAQSi": "", - "lbw10C": "", - "t262xH": "", - "DwevKz": "", - "Ppx673": "", - "JCMXwP": "", - "I7B7SH": "", - "OSI607": "", - "a9S/OH": "", - "HSv9BP": "", - "VL1Y/1": "", - "Ew1f8q": "", - "awr0AJ": "", - "CVsoUM": "", - "BRGcS0": "", - "9vqdq3": "", - "fFOayY": "", - "u81G9+": "", - "i2FBWn": "", - "G5Lt80": "", - "YV7rXP": "", - "6zzIEm": "", - "6pCzRs": "", - "zL23+z": "", - "79eRW1": "", - "dX7+Rv": "", - "m0fapd": "", - "sy+pv5": "", - "9Utk00": "", - "Eq5gCU": "", - "qp+wDV": "", - "d/jCcy": "", - "FlVuUh": "", - "8aUjqQ": "", - "5sg7KC": "", - "IGY48m": "", - "Pgb3Xj": "", - "WOH7Yj": "", - "L7z2/k": "", - "NOyDVq": "", - "tzMNF3": "", - "ZvKSfJ": "", - "xJrRMG": "", - "tKMlOc": "", - "krEziQ": "", - "BQBZU+": "", - "b9fOA1": "", - "80EXUh": "", - "dudqv/": "", - "2syGZB": "", - "W6nSYE": "", - "1P6GMj": "", - "7cv4Uf": "", - "/GKH/w": "", - "FDReLp": "", - "qDj0JR": "", - "CYs0LF": "", - "kkpP2k": "", - "Dnob31": "", - "VQDmmK": "", - "JOZGPR": "", - "GbVCQb": "", - "myq2ZL": "", - "KN7zKn": "", - "D3rOMr": "", - "uPb/gh": "", - "sjzDbu": "", - "q0qMyV": "", - "9y9QQh": "", - "waUHa4": "", - "cVqFq/": "", - "gczcC5": "", - "8OoV56": "", - "Auj/Ki": "", - "SyYroX": "", - "PSqtlY": "", - "rptmhC": "", - "2yCGR2": "", - "u5aHb4": "", - "p556q3": "", - "4l6vz1": "", - "45YlLU": "" + "Y9HHck": "S'authentifier", + "1QcGkA": "Entrez votre email pour recevoir un lien magique d'accès à la page de statut. Remarque : seuls les emails provenant de domaines approuvés sont acceptés.", + "wSZR47": "Envoyer", + "txkW56": "Envoi en cours...", + "OrFVks": "Vérifiez votre boîte de réception !", + "n36zhX": "Accédez à la page de statut en cliquant sur le lien dans l'email.", + "qIAQSi": "Page protégée", + "lbw10C": "Entrez le mot de passe pour accéder à la page de statut.", + "t262xH": "L'email et l'URL de redirection sont requis", + "DwevKz": "Une erreur inattendue s'est produite lors de la connexion", + "Ppx673": "Rapports", + "JCMXwP": "Maintenances", + "I7B7SH": "Aucune maintenance trouvée", + "OSI607": "Aucune maintenance trouvée pour cette page de statut.", + "a9S/OH": "Maintenance introuvable", + "HSv9BP": "La maintenance que vous recherchez n'existe pas.", + "VL1Y/1": "Rapport introuvable", + "Ew1f8q": "Le rapport que vous recherchez n'existe pas.", + "awr0AJ": "Moniteur introuvable", + "CVsoUM": "Le moniteur que vous recherchez n'existe pas.", + "BRGcS0": "Latence globale", + "9vqdq3": "Latence par région", + "fFOayY": "régions", + "u81G9+": "Disponibilité", + "i2FBWn": "vérifications", + "G5Lt80": "La latence agrégée de toutes les régions actives basée sur différents quantiles.", + "YV7rXP": "Latence par région", + "6zzIEm": "Latence par région au quantile p75, triée par région la plus lente. Comparez jusqu'à 6 régions.", + "6pCzRs": "Disponibilité totale", + "zL23+z": "Valeurs principales de disponibilité, en toute transparence.", + "79eRW1": "Confirmation en cours...", + "dX7+Rv": "Confirmé", + "m0fapd": "Échec de la confirmation", + "sy+pv5": "Email", + "9Utk00": "Mise à jour de l'abonnement...", + "Eq5gCU": "Abonnement mis à jour", + "qp+wDV": "Échec de la mise à jour de l'abonnement", + "d/jCcy": "S'abonner à des composants spécifiques", + "FlVuUh": "Aucun composant auquel s'abonner", + "8aUjqQ": "Cette page de statut n'a aucun composant auquel s'abonner.", + "5sg7KC": "Mot de passe", + "IGY48m": "Abonnement en cours...", + "Pgb3Xj": "Abonné", + "WOH7Yj": "Échec de l'abonnement", + "L7z2/k": "Cette page n'a aucun composant auquel s'abonner.", + "NOyDVq": "propulsé par", + "tzMNF3": "Statut", + "ZvKSfJ": "Événements", + "xJrRMG": "Moniteurs", + "tKMlOc": "Menu", + "krEziQ": "Nous contacter", + "BQBZU+": "Tous les systèmes sont opérationnels", + "b9fOA1": "Performances dégradées", + "80EXUh": "Performances en panne", + "dudqv/": "Maintenance", + "2syGZB": "Rapport résolu", + "W6nSYE": "Résolu", + "1P6GMj": "Surveillance", + "7cv4Uf": "Identifié", + "/GKH/w": "En cours d'investigation", + "FDReLp": "Aucune notification récente", + "qDj0JR": "Aucun rapport au cours des 7 derniers jours.", + "CYs0LF": "Voir l'historique des événements", + "kkpP2k": "aujourd'hui", + "Dnob31": "Opérationnel", + "VQDmmK": "Dégradé", + "JOZGPR": "En panne", + "GbVCQb": "Cliquez à nouveau pour désépingler", + "myq2ZL": "Normal", + "KN7zKn": "Erreur", + "D3rOMr": "Aucune donnée", + "uPb/gh": "Recevoir les mises à jour", + "sjzDbu": "Slack", + "q0qMyV": "RSS", + "9y9QQh": "JSON", + "waUHa4": "SSH", + "cVqFq/": "Recevez des notifications par email à chaque création ou résolution d'un rapport", + "gczcC5": "S'abonner", + "8OoV56": "Obtenir le flux RSS", + "Auj/Ki": "Obtenir le flux Atom", + "SyYroX": "Obtenir les mises à jour JSON", + "PSqtlY": "Obtenir le statut via SSH", + "rptmhC": "Pour recevoir les mises à jour dans Slack, collez le texte ci-dessous dans n'importe quel canal.", + "2yCGR2": "Lien copié dans le presse-papiers", + "u5aHb4": "Copier le lien", + "45YlLU": "Validez votre email pour recevoir les mises à jour et le tour est joué." } diff --git a/apps/status-page/messages/ja.json b/apps/status-page/messages/ja.json deleted file mode 100644 index 91c6836b11..0000000000 --- a/apps/status-page/messages/ja.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "Y9HHck": "", - "1QcGkA": "", - "txkW56": "", - "wSZR47": "", - "OrFVks": "", - "n36zhX": "", - "qIAQSi": "", - "lbw10C": "", - "t262xH": "", - "DwevKz": "", - "Ppx673": "", - "JCMXwP": "", - "I7B7SH": "", - "OSI607": "", - "a9S/OH": "", - "HSv9BP": "", - "VL1Y/1": "", - "Ew1f8q": "", - "awr0AJ": "", - "CVsoUM": "", - "BRGcS0": "", - "9vqdq3": "", - "fFOayY": "", - "u81G9+": "", - "i2FBWn": "", - "G5Lt80": "", - "YV7rXP": "", - "6zzIEm": "", - "6pCzRs": "", - "zL23+z": "", - "79eRW1": "", - "dX7+Rv": "", - "m0fapd": "", - "sy+pv5": "", - "9Utk00": "", - "Eq5gCU": "", - "qp+wDV": "", - "d/jCcy": "", - "FlVuUh": "", - "8aUjqQ": "", - "5sg7KC": "", - "IGY48m": "", - "Pgb3Xj": "", - "WOH7Yj": "", - "L7z2/k": "", - "NOyDVq": "", - "tzMNF3": "", - "ZvKSfJ": "", - "xJrRMG": "", - "tKMlOc": "", - "krEziQ": "", - "BQBZU+": "", - "b9fOA1": "", - "80EXUh": "", - "dudqv/": "", - "2syGZB": "", - "W6nSYE": "", - "1P6GMj": "", - "7cv4Uf": "", - "/GKH/w": "", - "FDReLp": "", - "qDj0JR": "", - "CYs0LF": "", - "kkpP2k": "", - "Dnob31": "", - "VQDmmK": "", - "JOZGPR": "", - "GbVCQb": "", - "myq2ZL": "", - "KN7zKn": "", - "D3rOMr": "", - "uPb/gh": "", - "sjzDbu": "", - "q0qMyV": "", - "9y9QQh": "", - "waUHa4": "", - "cVqFq/": "", - "gczcC5": "", - "8OoV56": "", - "Auj/Ki": "", - "SyYroX": "", - "PSqtlY": "", - "rptmhC": "", - "2yCGR2": "", - "u5aHb4": "", - "p556q3": "", - "4l6vz1": "", - "45YlLU": "" -} diff --git a/apps/status-page/messages/ko.json b/apps/status-page/messages/ko.json deleted file mode 100644 index 91c6836b11..0000000000 --- a/apps/status-page/messages/ko.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "Y9HHck": "", - "1QcGkA": "", - "txkW56": "", - "wSZR47": "", - "OrFVks": "", - "n36zhX": "", - "qIAQSi": "", - "lbw10C": "", - "t262xH": "", - "DwevKz": "", - "Ppx673": "", - "JCMXwP": "", - "I7B7SH": "", - "OSI607": "", - "a9S/OH": "", - "HSv9BP": "", - "VL1Y/1": "", - "Ew1f8q": "", - "awr0AJ": "", - "CVsoUM": "", - "BRGcS0": "", - "9vqdq3": "", - "fFOayY": "", - "u81G9+": "", - "i2FBWn": "", - "G5Lt80": "", - "YV7rXP": "", - "6zzIEm": "", - "6pCzRs": "", - "zL23+z": "", - "79eRW1": "", - "dX7+Rv": "", - "m0fapd": "", - "sy+pv5": "", - "9Utk00": "", - "Eq5gCU": "", - "qp+wDV": "", - "d/jCcy": "", - "FlVuUh": "", - "8aUjqQ": "", - "5sg7KC": "", - "IGY48m": "", - "Pgb3Xj": "", - "WOH7Yj": "", - "L7z2/k": "", - "NOyDVq": "", - "tzMNF3": "", - "ZvKSfJ": "", - "xJrRMG": "", - "tKMlOc": "", - "krEziQ": "", - "BQBZU+": "", - "b9fOA1": "", - "80EXUh": "", - "dudqv/": "", - "2syGZB": "", - "W6nSYE": "", - "1P6GMj": "", - "7cv4Uf": "", - "/GKH/w": "", - "FDReLp": "", - "qDj0JR": "", - "CYs0LF": "", - "kkpP2k": "", - "Dnob31": "", - "VQDmmK": "", - "JOZGPR": "", - "GbVCQb": "", - "myq2ZL": "", - "KN7zKn": "", - "D3rOMr": "", - "uPb/gh": "", - "sjzDbu": "", - "q0qMyV": "", - "9y9QQh": "", - "waUHa4": "", - "cVqFq/": "", - "gczcC5": "", - "8OoV56": "", - "Auj/Ki": "", - "SyYroX": "", - "PSqtlY": "", - "rptmhC": "", - "2yCGR2": "", - "u5aHb4": "", - "p556q3": "", - "4l6vz1": "", - "45YlLU": "" -} diff --git a/apps/status-page/messages/pt.json b/apps/status-page/messages/pt.json deleted file mode 100644 index 91c6836b11..0000000000 --- a/apps/status-page/messages/pt.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "Y9HHck": "", - "1QcGkA": "", - "txkW56": "", - "wSZR47": "", - "OrFVks": "", - "n36zhX": "", - "qIAQSi": "", - "lbw10C": "", - "t262xH": "", - "DwevKz": "", - "Ppx673": "", - "JCMXwP": "", - "I7B7SH": "", - "OSI607": "", - "a9S/OH": "", - "HSv9BP": "", - "VL1Y/1": "", - "Ew1f8q": "", - "awr0AJ": "", - "CVsoUM": "", - "BRGcS0": "", - "9vqdq3": "", - "fFOayY": "", - "u81G9+": "", - "i2FBWn": "", - "G5Lt80": "", - "YV7rXP": "", - "6zzIEm": "", - "6pCzRs": "", - "zL23+z": "", - "79eRW1": "", - "dX7+Rv": "", - "m0fapd": "", - "sy+pv5": "", - "9Utk00": "", - "Eq5gCU": "", - "qp+wDV": "", - "d/jCcy": "", - "FlVuUh": "", - "8aUjqQ": "", - "5sg7KC": "", - "IGY48m": "", - "Pgb3Xj": "", - "WOH7Yj": "", - "L7z2/k": "", - "NOyDVq": "", - "tzMNF3": "", - "ZvKSfJ": "", - "xJrRMG": "", - "tKMlOc": "", - "krEziQ": "", - "BQBZU+": "", - "b9fOA1": "", - "80EXUh": "", - "dudqv/": "", - "2syGZB": "", - "W6nSYE": "", - "1P6GMj": "", - "7cv4Uf": "", - "/GKH/w": "", - "FDReLp": "", - "qDj0JR": "", - "CYs0LF": "", - "kkpP2k": "", - "Dnob31": "", - "VQDmmK": "", - "JOZGPR": "", - "GbVCQb": "", - "myq2ZL": "", - "KN7zKn": "", - "D3rOMr": "", - "uPb/gh": "", - "sjzDbu": "", - "q0qMyV": "", - "9y9QQh": "", - "waUHa4": "", - "cVqFq/": "", - "gczcC5": "", - "8OoV56": "", - "Auj/Ki": "", - "SyYroX": "", - "PSqtlY": "", - "rptmhC": "", - "2yCGR2": "", - "u5aHb4": "", - "p556q3": "", - "4l6vz1": "", - "45YlLU": "" -} diff --git a/apps/status-page/messages/zh.json b/apps/status-page/messages/zh.json deleted file mode 100644 index 91c6836b11..0000000000 --- a/apps/status-page/messages/zh.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "Y9HHck": "", - "1QcGkA": "", - "txkW56": "", - "wSZR47": "", - "OrFVks": "", - "n36zhX": "", - "qIAQSi": "", - "lbw10C": "", - "t262xH": "", - "DwevKz": "", - "Ppx673": "", - "JCMXwP": "", - "I7B7SH": "", - "OSI607": "", - "a9S/OH": "", - "HSv9BP": "", - "VL1Y/1": "", - "Ew1f8q": "", - "awr0AJ": "", - "CVsoUM": "", - "BRGcS0": "", - "9vqdq3": "", - "fFOayY": "", - "u81G9+": "", - "i2FBWn": "", - "G5Lt80": "", - "YV7rXP": "", - "6zzIEm": "", - "6pCzRs": "", - "zL23+z": "", - "79eRW1": "", - "dX7+Rv": "", - "m0fapd": "", - "sy+pv5": "", - "9Utk00": "", - "Eq5gCU": "", - "qp+wDV": "", - "d/jCcy": "", - "FlVuUh": "", - "8aUjqQ": "", - "5sg7KC": "", - "IGY48m": "", - "Pgb3Xj": "", - "WOH7Yj": "", - "L7z2/k": "", - "NOyDVq": "", - "tzMNF3": "", - "ZvKSfJ": "", - "xJrRMG": "", - "tKMlOc": "", - "krEziQ": "", - "BQBZU+": "", - "b9fOA1": "", - "80EXUh": "", - "dudqv/": "", - "2syGZB": "", - "W6nSYE": "", - "1P6GMj": "", - "7cv4Uf": "", - "/GKH/w": "", - "FDReLp": "", - "qDj0JR": "", - "CYs0LF": "", - "kkpP2k": "", - "Dnob31": "", - "VQDmmK": "", - "JOZGPR": "", - "GbVCQb": "", - "myq2ZL": "", - "KN7zKn": "", - "D3rOMr": "", - "uPb/gh": "", - "sjzDbu": "", - "q0qMyV": "", - "9y9QQh": "", - "waUHa4": "", - "cVqFq/": "", - "gczcC5": "", - "8OoV56": "", - "Auj/Ki": "", - "SyYroX": "", - "PSqtlY": "", - "rptmhC": "", - "2yCGR2": "", - "u5aHb4": "", - "p556q3": "", - "4l6vz1": "", - "45YlLU": "" -} diff --git a/apps/status-page/next.config.ts b/apps/status-page/next.config.ts index c73949080d..11f018a32e 100644 --- a/apps/status-page/next.config.ts +++ b/apps/status-page/next.config.ts @@ -1,3 +1,4 @@ +import { defaultLocale, locales } from "@/i18n/config"; import { withSentryConfig } from "@sentry/nextjs"; import type { NextConfig } from "next"; import createNextIntlPlugin from "next-intl/plugin"; @@ -7,12 +8,12 @@ const withNextIntl = createNextIntlPlugin({ experimental: { srcPath: "./src", extract: { - sourceLocale: "en", + sourceLocale: defaultLocale, }, messages: { path: "./messages", format: "json", - locales: "infer", + locales, }, }, }); @@ -33,70 +34,6 @@ const nextConfig: NextConfig = { fullUrl: true, }, }, - async rewrites() { - return { - beforeFiles: [ - // When URL already has a locale prefix (e.g. /fr/events → /subdomain/fr/events) - { - source: "/:locale(en|es|fr|de|pt|ja|zh|ko)/:path*", - has: [ - { - type: "host", - value: - process.env.NODE_ENV === "production" - ? "(?[^.]+).stpg.dev" - : "(?[^.]+).localhost", - }, - ], - missing: [ - { - type: "header", - key: "x-proxy", - value: "1", - }, - { - type: "host", - value: - process.env.NODE_ENV === "production" - ? "www.stpg.dev" - : "localhost", - }, - ], - destination: "/:subdomain/:locale/:path*", - }, - // When URL has no locale prefix (e.g. /events → /subdomain/en/events) - { - source: - "/:path((?!api|assets|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)", - has: [ - { - type: "host", - value: - process.env.NODE_ENV === "production" - ? "(?[^.]+).stpg.dev" - : "(?[^.]+).localhost", - }, - ], - missing: [ - // Skip this rewrite when the request came via proxy from web app - { - type: "header", - key: "x-proxy", - value: "1", - }, - { - type: "host", - value: - process.env.NODE_ENV === "production" - ? "www.stpg.dev" - : "localhost", - }, - ], - destination: "/:subdomain/en/:path*", - }, - ], - }; - }, }; // For detailed options, refer to the official documentation: diff --git a/apps/status-page/src/components/content/timestamp-hover-card.tsx b/apps/status-page/src/components/content/timestamp-hover-card.tsx index 8189df5b4b..e30ae30ab3 100644 --- a/apps/status-page/src/components/content/timestamp-hover-card.tsx +++ b/apps/status-page/src/components/content/timestamp-hover-card.tsx @@ -1,3 +1,5 @@ +"use client"; + import { UTCDate } from "@date-fns/utc"; import { HoverCard, diff --git a/apps/status-page/src/components/language-switcher.tsx b/apps/status-page/src/components/language-switcher.tsx deleted file mode 100644 index 2b2a1efd59..0000000000 --- a/apps/status-page/src/components/language-switcher.tsx +++ /dev/null @@ -1,84 +0,0 @@ -"use client"; - -import { locales } from "@/i18n/config"; -import { Button } from "@openstatus/ui/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@openstatus/ui/components/ui/dropdown-menu"; -import { GlobeIcon } from "lucide-react"; -import { useLocale } from "next-intl"; -import { useParams, usePathname, useRouter } from "next/navigation"; -import { useTransition } from "react"; - -const localeNames: Record = { - en: "English", - es: "Español", - fr: "Français", - de: "Deutsch", - pt: "Português", - ja: "日本語", - zh: "中文", - ko: "한국어", -}; - -export function LanguageSwitcher() { - const locale = useLocale(); - const router = useRouter(); - const pathname = usePathname(); - const params = useParams(); - const [isPending, startTransition] = useTransition(); - - function onSelectLocale(nextLocale: string) { - startTransition(() => { - const segments = pathname.split("/"); - const domain = params.domain as string; - const domainIndex = segments.indexOf(domain); - - const potentialLocale = segments[domainIndex + 1]; - const hasLocaleSegment = (locales as readonly string[]).includes( - potentialLocale, - ); - - if (nextLocale === "en") { - if (hasLocaleSegment) { - segments.splice(domainIndex + 1, 1); - } - } else if (hasLocaleSegment) { - segments[domainIndex + 1] = nextLocale; - } else { - segments.splice(domainIndex + 1, 0, nextLocale); - } - - router.replace(segments.join("/") || "/"); - }); - } - - return ( - - - - - - {locales.map((loc) => ( - onSelectLocale(loc)} - className={loc === locale ? "font-bold" : ""} - > - {localeNames[loc]} - - ))} - - - ); -} diff --git a/apps/status-page/src/components/locale-switcher.tsx b/apps/status-page/src/components/locale-switcher.tsx new file mode 100644 index 0000000000..cc6f693393 --- /dev/null +++ b/apps/status-page/src/components/locale-switcher.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { defaultLocale, localeTranslations, locales } from "@/i18n/config"; +import { cn } from "@/lib/utils"; +import { Button } from "@openstatus/ui/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@openstatus/ui/components/ui/dropdown-menu"; +import { Skeleton } from "@openstatus/ui/components/ui/skeleton"; +import { useLocale } from "next-intl"; +import { useParams, useRouter } from "next/navigation"; +import { useEffect, useState, useTransition } from "react"; + +export function LocaleSwitcher({ + className, + ...props +}: React.ComponentProps) { + const locale = useLocale(); + const router = useRouter(); + const params = useParams(); + const [isPending, startTransition] = useTransition(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + function onSelectLocale(nextLocale: string) { + startTransition(() => { + // Use the browser pathname — usePathname() returns the rewritten path + // which always includes the slug, even for hostname routing. + const browserPath = window.location.pathname; + const segments = browserPath.split("/"); + const domain = params.domain as string; + + // Pathname routing: slug is in the browser URL (e.g., /acme/fr/events) + // Hostname routing: slug is NOT in the browser URL (e.g., /fr/events) + const isPathnameRouting = segments.includes(domain); + const localeIndex = isPathnameRouting ? 2 : 1; + const hasLocaleSegment = (locales as readonly string[]).includes( + segments[localeIndex] as (typeof locales)[number], + ); + + // Pathname routing always keeps the locale segment (including default). + // Hostname routing omits the default locale from the URL. + const omitLocale = nextLocale === defaultLocale && !isPathnameRouting; + + if (omitLocale) { + if (hasLocaleSegment) { + segments.splice(localeIndex, 1); + } + } else if (hasLocaleSegment) { + segments[localeIndex] = nextLocale; + } else { + segments.splice(localeIndex, 0, nextLocale); + } + + router.replace(segments.join("/") || "/"); + }); + } + + if (!mounted) { + return ; + } + + return ( + + + + + + + {Object.entries(localeTranslations).map(([key, { name }]) => ( + onSelectLocale(key)}> + {name} + + ))} + + + + ); +} diff --git a/apps/status-page/src/components/nav/footer.tsx b/apps/status-page/src/components/nav/footer.tsx index 0370ab2219..1a23b6c8c8 100644 --- a/apps/status-page/src/components/nav/footer.tsx +++ b/apps/status-page/src/components/nav/footer.tsx @@ -2,7 +2,7 @@ import { Link } from "@/components/common/link"; import { TimestampHoverCard } from "@/components/content/timestamp-hover-card"; -import { LanguageSwitcher } from "@/components/language-switcher"; +import { LocaleSwitcher } from "@/components/locale-switcher"; import { ThemeDropdown } from "@/components/themes/theme-dropdown"; import { useTRPC } from "@/lib/trpc/client"; import { Skeleton } from "@openstatus/ui/components/ui/skeleton"; @@ -45,7 +45,7 @@ export function Footer(props: React.ComponentProps<"footer">) {

) : null}
-
+
) { )} - +
diff --git a/apps/status-page/src/components/nav/header.tsx b/apps/status-page/src/components/nav/header.tsx index e9e40ffa28..42d02c4a0e 100644 --- a/apps/status-page/src/components/nav/header.tsx +++ b/apps/status-page/src/components/nav/header.tsx @@ -6,6 +6,7 @@ import { StatusUpdates, } from "@/components/status-page/status-updates"; import { usePathnamePrefix } from "@/hooks/use-pathname-prefix"; +import { defaultLocale } from "@/i18n/config"; import { useTRPC } from "@/lib/trpc/client"; import type { RouterOutputs } from "@openstatus/api"; import { Button } from "@openstatus/ui/components/ui/button"; @@ -26,7 +27,7 @@ import { cn } from "@openstatus/ui/lib/utils"; import { useMutation, useQuery } from "@tanstack/react-query"; import { isTRPCClientError } from "@trpc/client"; import { Menu, MessageCircleMore } from "lucide-react"; -import { useExtracted } from "next-intl"; +import { useExtracted, useLocale } from "next-intl"; import NextLink from "next/link"; import { useParams, usePathname } from "next/navigation"; import { useState } from "react"; @@ -41,16 +42,19 @@ function useNav() { return [ { + key: "status", label: t("Status"), href: `/${prefix}`, isActive: pathname === `/${prefix}`, }, { + key: "events", label: t("Events"), href: `${prefix ? `/${prefix}` : ""}/events`, isActive: pathname.startsWith(`${prefix ? `/${prefix}` : ""}/events`), }, { + key: "monitors", label: t("Monitors"), href: `${prefix ? `/${prefix}` : ""}/monitors`, isActive: pathname.startsWith(`${prefix ? `/${prefix}` : ""}/monitors`), @@ -76,6 +80,7 @@ function getStatusUpdateTypes(page: Page): StatusUpdateType[] { export function Header(props: React.ComponentProps<"header">) { const t = useExtracted(); const trpc = useTRPC(); + const locale = useLocale(); const { domain } = useParams<{ domain: string }>(); const { data: page } = useQuery({ ...trpc.statusPage.get.queryOptions({ slug: domain }), @@ -118,7 +123,11 @@ export function Header(props: React.ComponentProps<"header">) { asChild > @@ -168,7 +177,7 @@ function NavDesktop({ className, ...props }: React.ComponentProps<"ul">) {
    {nav.map((item) => { return ( -
  • +
- {formatDateRange(from, to ?? undefined)}{" "} + {formatDateRange(from, to ?? undefined, locale)}{" "} {formatDuration({ from, to, name, status })} diff --git a/apps/status-page/src/lib/formatter.ts b/apps/status-page/src/lib/formatter.ts index ec131fada2..7ecfe22349 100644 --- a/apps/status-page/src/lib/formatter.ts +++ b/apps/status-page/src/lib/formatter.ts @@ -41,17 +41,21 @@ export function formatNumber( // TODO: think of supporting custom formats -export function formatDate(date: Date, options?: Intl.DateTimeFormatOptions) { - return date.toLocaleDateString("en-US", { +export function formatDate( + date: Date, + options?: Intl.DateTimeFormatOptions & { locale?: string }, +) { + const { locale, ...rest } = options ?? {}; + return date.toLocaleDateString(locale, { year: "numeric", month: "long", day: "numeric", - ...options, + ...rest, }); } -export function formatDateTime(date: Date) { - return date.toLocaleDateString("en-US", { +export function formatDateTime(date: Date, locale?: string) { + return date.toLocaleDateString(locale, { month: "long", day: "numeric", hour: "numeric", @@ -59,40 +63,40 @@ export function formatDateTime(date: Date) { }); } -export function formatTime(date: Date) { - return date.toLocaleTimeString("en-US", { +export function formatTime(date: Date, locale?: string) { + return date.toLocaleTimeString(locale, { hour: "numeric", minute: "numeric", }); } -export function formatDateRange(from?: Date, to?: Date) { +export function formatDateRange(from?: Date, to?: Date, locale?: string) { const sameDay = from && to && isSameDay(from, to); const isFromStartDay = from && startOfDay(from).getTime() === from.getTime(); const isToEndDay = to && endOfDay(to).getTime() === to.getTime(); if (sameDay) { if (from.getTime() === to.getTime()) { - return formatDateTime(from); + return formatDateTime(from, locale); } if (from && to) { - return `${formatDateTime(from)} - ${formatTime(to)}`; + return `${formatDateTime(from, locale)} - ${formatTime(to, locale)}`; } } if (from && to) { if (isFromStartDay && isToEndDay) { - return `${formatDate(from)} - ${formatDate(to)}`; + return `${formatDate(from, { locale })} - ${formatDate(to, { locale })}`; } - return `${formatDateTime(from)} - ${formatDateTime(to)}`; + return `${formatDateTime(from, locale)} - ${formatDateTime(to, locale)}`; } if (to) { - return `Until ${formatDateTime(to)}`; + return `Until ${formatDateTime(to, locale)}`; } if (from) { - return `Since ${formatDateTime(from)}`; + return `Since ${formatDateTime(from, locale)}`; } return "All time"; From c4b8ccf72ee25eae25643821d95eb6032a1f6dd9 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Fri, 20 Mar 2026 20:00:07 +0100 Subject: [PATCH 19/24] fix: locale navigation --- .../src/app/(status-page)/[domain]/layout.tsx | 2 +- apps/status-page/src/components/nav/header.tsx | 11 +++-------- .../src/lib/resolve-pathname-prefix.test.ts | 14 +++++++------- .../status-page/src/lib/resolve-pathname-prefix.ts | 2 +- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/apps/status-page/src/app/(status-page)/[domain]/layout.tsx b/apps/status-page/src/app/(status-page)/[domain]/layout.tsx index 8b1585655b..b52958cb19 100644 --- a/apps/status-page/src/app/(status-page)/[domain]/layout.tsx +++ b/apps/status-page/src/app/(status-page)/[domain]/layout.tsx @@ -17,7 +17,7 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { z } from "zod"; -export const schema = z.object({ +const schema = z.object({ value: z.enum(["duration", "requests", "manual"]).prefault("duration"), type: z.enum(["absolute", "manual"]).prefault("absolute"), uptime: z.coerce.boolean().prefault(true), diff --git a/apps/status-page/src/components/nav/header.tsx b/apps/status-page/src/components/nav/header.tsx index 42d02c4a0e..2e40ed3615 100644 --- a/apps/status-page/src/components/nav/header.tsx +++ b/apps/status-page/src/components/nav/header.tsx @@ -6,7 +6,6 @@ import { StatusUpdates, } from "@/components/status-page/status-updates"; import { usePathnamePrefix } from "@/hooks/use-pathname-prefix"; -import { defaultLocale } from "@/i18n/config"; import { useTRPC } from "@/lib/trpc/client"; import type { RouterOutputs } from "@openstatus/api"; import { Button } from "@openstatus/ui/components/ui/button"; @@ -27,7 +26,7 @@ import { cn } from "@openstatus/ui/lib/utils"; import { useMutation, useQuery } from "@tanstack/react-query"; import { isTRPCClientError } from "@trpc/client"; import { Menu, MessageCircleMore } from "lucide-react"; -import { useExtracted, useLocale } from "next-intl"; +import { useExtracted } from "next-intl"; import NextLink from "next/link"; import { useParams, usePathname } from "next/navigation"; import { useState } from "react"; @@ -80,11 +79,11 @@ function getStatusUpdateTypes(page: Page): StatusUpdateType[] { export function Header(props: React.ComponentProps<"header">) { const t = useExtracted(); const trpc = useTRPC(); - const locale = useLocale(); const { domain } = useParams<{ domain: string }>(); const { data: page } = useQuery({ ...trpc.statusPage.get.queryOptions({ slug: domain }), }); + const prefix = usePathnamePrefix(); const sendPageSubscriptionMutation = useMutation( trpc.emailRouter.sendPageSubscriptionVerification.mutationOptions({}), @@ -123,11 +122,7 @@ export function Header(props: React.ComponentProps<"header">) { asChild > diff --git a/apps/status-page/src/lib/resolve-pathname-prefix.test.ts b/apps/status-page/src/lib/resolve-pathname-prefix.test.ts index 4f31625ffe..32231b9a15 100644 --- a/apps/status-page/src/lib/resolve-pathname-prefix.test.ts +++ b/apps/status-page/src/lib/resolve-pathname-prefix.test.ts @@ -81,7 +81,7 @@ describe("resolvePathnamePrefix", () => { }); describe("pathname routing", () => { - test("localhost + /acme + en → 'acme' (default locale omitted)", () => { + test("localhost + /acme + en → 'acme/en'", () => { expect( resolvePathnamePrefix({ hostname: "localhost", @@ -90,7 +90,7 @@ describe("resolvePathnamePrefix", () => { locale: "en", defaultLocale, }), - ).toBe("acme"); + ).toBe("acme/en"); }); test("localhost + /acme + fr → 'acme/fr'", () => { @@ -105,7 +105,7 @@ describe("resolvePathnamePrefix", () => { ).toBe("acme/fr"); }); - test("localhost + /acme/en/events + en → 'acme' (default locale omitted)", () => { + test("localhost + /acme/en/events + en → 'acme/en'", () => { expect( resolvePathnamePrefix({ hostname: "localhost", @@ -114,7 +114,7 @@ describe("resolvePathnamePrefix", () => { locale: "en", defaultLocale, }), - ).toBe("acme"); + ).toBe("acme/en"); }); test("localhost + /acme/fr/monitors/123 + fr → 'acme/fr'", () => { @@ -129,7 +129,7 @@ describe("resolvePathnamePrefix", () => { ).toBe("acme/fr"); }); - test("localhost + /status + en → 'status' (default locale omitted)", () => { + test("localhost + /status + en → 'status/en'", () => { expect( resolvePathnamePrefix({ hostname: "localhost", @@ -138,7 +138,7 @@ describe("resolvePathnamePrefix", () => { locale: "en", defaultLocale, }), - ).toBe("status"); + ).toBe("status/en"); }); test("localhost + /status/fr/events + fr → 'status/fr'", () => { @@ -164,7 +164,7 @@ describe("resolvePathnamePrefix", () => { locale: "en", defaultLocale, }), - ).toBe("acme"); + ).toBe("acme/en"); }); test("vercel.app preview is treated as pathname routing", () => { diff --git a/apps/status-page/src/lib/resolve-pathname-prefix.ts b/apps/status-page/src/lib/resolve-pathname-prefix.ts index 4502921ebe..5deb4d6833 100644 --- a/apps/status-page/src/lib/resolve-pathname-prefix.ts +++ b/apps/status-page/src/lib/resolve-pathname-prefix.ts @@ -37,5 +37,5 @@ export function resolvePathnamePrefix({ // Pathname routing — slug always, locale only if non-default const slug = pathname.split("/")[1] || ""; - return locale !== defaultLocale ? `${slug}/${locale}` : slug; + return `${slug}/${locale}`; } From 7560c1ce0a4bc5ce86e53865e8c570a5bf1a1e66 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Fri, 20 Mar 2026 20:41:38 +0100 Subject: [PATCH 20/24] chore: locale switcher --- apps/status-page/src/components/locale-switcher.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/status-page/src/components/locale-switcher.tsx b/apps/status-page/src/components/locale-switcher.tsx index cc6f693393..715bfb392d 100644 --- a/apps/status-page/src/components/locale-switcher.tsx +++ b/apps/status-page/src/components/locale-switcher.tsx @@ -83,7 +83,15 @@ export function LocaleSwitcher({ {Object.entries(localeTranslations).map(([key, { name }]) => ( onSelectLocale(key)}> - {name} + {name}{" "} + + {key} + ))} From 3fd58bf0be739c0728d5a1aebdcfa1cf7af0f0d8 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Fri, 20 Mar 2026 21:02:04 +0100 Subject: [PATCH 21/24] chore: format theme switcher --- apps/status-page/messages/de.json | 2 +- apps/status-page/messages/en.json | 2 +- apps/status-page/messages/fr.json | 2 +- .../src/components/locale-switcher.tsx | 2 +- .../status-page/src/components/nav/footer.tsx | 4 +-- .../src/components/themes/theme-dropdown.tsx | 33 ++++++++++++------- 6 files changed, 27 insertions(+), 18 deletions(-) diff --git a/apps/status-page/messages/de.json b/apps/status-page/messages/de.json index b9d7b2a166..bb69568470 100644 --- a/apps/status-page/messages/de.json +++ b/apps/status-page/messages/de.json @@ -1,8 +1,8 @@ { "Y9HHck": "Authentifizieren", "1QcGkA": "Geben Sie Ihre E-Mail-Adresse ein, um einen Magic Link für den Zugriff auf die Statusseite zu erhalten. Hinweis: Es werden nur E-Mails von genehmigten Domains akzeptiert.", - "wSZR47": "Absenden", "txkW56": "Wird gesendet...", + "wSZR47": "Absenden", "OrFVks": "Überprüfen Sie Ihren Posteingang!", "n36zhX": "Greifen Sie auf die Statusseite zu, indem Sie auf den Link in der E-Mail klicken.", "qIAQSi": "Geschützte Seite", diff --git a/apps/status-page/messages/en.json b/apps/status-page/messages/en.json index 563ebb6972..06107aac29 100644 --- a/apps/status-page/messages/en.json +++ b/apps/status-page/messages/en.json @@ -1,8 +1,8 @@ { "Y9HHck": "Authenticate", "1QcGkA": "Enter your email to receive a magic link for accessing the status page. Note: Only emails from approved domains are accepted.", - "wSZR47": "Submit", "txkW56": "Submitting...", + "wSZR47": "Submit", "OrFVks": "Check your inbox!", "n36zhX": "Access the status page by clicking the link in the email.", "qIAQSi": "Protected Page", diff --git a/apps/status-page/messages/fr.json b/apps/status-page/messages/fr.json index 23141c4e56..f97826335d 100644 --- a/apps/status-page/messages/fr.json +++ b/apps/status-page/messages/fr.json @@ -1,8 +1,8 @@ { "Y9HHck": "S'authentifier", "1QcGkA": "Entrez votre email pour recevoir un lien magique d'accès à la page de statut. Remarque : seuls les emails provenant de domaines approuvés sont acceptés.", - "wSZR47": "Envoyer", "txkW56": "Envoi en cours...", + "wSZR47": "Envoyer", "OrFVks": "Vérifiez votre boîte de réception !", "n36zhX": "Accédez à la page de statut en cliquant sur le lien dans l'email.", "qIAQSi": "Page protégée", diff --git a/apps/status-page/src/components/locale-switcher.tsx b/apps/status-page/src/components/locale-switcher.tsx index 715bfb392d..1b64188f13 100644 --- a/apps/status-page/src/components/locale-switcher.tsx +++ b/apps/status-page/src/components/locale-switcher.tsx @@ -79,7 +79,7 @@ export function LocaleSwitcher({ {locale} - + {Object.entries(localeTranslations).map(([key, { name }]) => ( onSelectLocale(key)}> diff --git a/apps/status-page/src/components/nav/footer.tsx b/apps/status-page/src/components/nav/footer.tsx index 1a23b6c8c8..f12a600191 100644 --- a/apps/status-page/src/components/nav/footer.tsx +++ b/apps/status-page/src/components/nav/footer.tsx @@ -45,12 +45,12 @@ export function Footer(props: React.ComponentProps<"footer">) {

) : null}
-
+
{isMounted ? ( <> diff --git a/apps/status-page/src/components/themes/theme-dropdown.tsx b/apps/status-page/src/components/themes/theme-dropdown.tsx index a3ceb49a1b..e957913ba6 100644 --- a/apps/status-page/src/components/themes/theme-dropdown.tsx +++ b/apps/status-page/src/components/themes/theme-dropdown.tsx @@ -16,10 +16,11 @@ import { Laptop, Moon, Sun } from "lucide-react"; import { useState } from "react"; import { useEffect } from "react"; -function getThemeIcon(theme?: string | null) { - if (theme === "light") return ; - if (theme === "dark") return ; - if (theme === "system") return ; +function getThemeIcon(theme?: string | null, className?: string) { + if (theme === "light") return ; + if (theme === "dark") return ; + if (theme === "system") + return ; return null; } @@ -41,18 +42,26 @@ export function ThemeDropdown({ return ( - - - {["light", "dark", "system"].map((theme) => ( - setTheme(theme)}> - {getThemeIcon(theme)} - {theme} - - ))} + + {["light", "dark", "system"].map((t) => { + const isActive = t === theme; + return ( + setTheme(t)}> + {t} + + {getThemeIcon( + t, + isActive ? "text-foreground" : "text-muted-foreground", + )} + + + ); + })} ); From b8030ec008526b4b85cde70db812b149f4f0dfe5 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Fri, 20 Mar 2026 21:58:11 +0100 Subject: [PATCH 22/24] fix: missing translations --- apps/status-page/messages/de.json | 47 ++++++++++++++- apps/status-page/messages/en.json | 47 ++++++++++++++- apps/status-page/messages/fr.json | 47 ++++++++++++++- apps/status-page/src/app/(public)/layout.tsx | 57 +++++++++++-------- .../[domain]/[locale]/(auth)/login/actions.ts | 2 +- .../[locale]/(public)/manage/[token]/page.tsx | 52 ++++++++++------- .../(public)/unsubscribe/[token]/page.tsx | 53 ++++++++++------- .../[locale]/(public)/verify/[token]/page.tsx | 14 +++-- .../src/components/button/button-back.tsx | 4 +- .../components/button/button-copy-link.tsx | 6 +- .../components/chart/chart-legend-badge.tsx | 4 +- .../content/timestamp-hover-card.tsx | 4 +- .../src/components/date-picker.tsx | 10 ++-- .../components/popover/popover-quantile.tsx | 12 ++-- .../components/status-page/status-blank.tsx | 27 ++++++--- .../components/status-page/status-events.tsx | 7 ++- .../components/status-page/status-tracker.tsx | 20 ++++--- 17 files changed, 299 insertions(+), 114 deletions(-) diff --git a/apps/status-page/messages/de.json b/apps/status-page/messages/de.json index bb69568470..268a62964d 100644 --- a/apps/status-page/messages/de.json +++ b/apps/status-page/messages/de.json @@ -17,6 +17,17 @@ "HSv9BP": "Die gesuchte Wartung existiert nicht.", "VL1Y/1": "Bericht nicht gefunden", "Ew1f8q": "Der gesuchte Bericht existiert nicht.", + "wkVkCX": "Erfolgreich abgemeldet", + "9qFG9F": "Abmeldung fehlgeschlagen", + "PV34S9": "Ungültiger Abonnement-Token", + "ar0fZ/": "Dieser Abonnement-Token ist nicht mehr gültig. Möglicherweise haben Sie sich bereits abgemeldet oder der Link ist abgelaufen.", + "orvpWh": "Zurück", + "K8kTfz": "Verwalten Sie Ihr Abonnement, um Updates zur Statusseite zu erhalten.", + "3JgeEq": "Abgemeldet am {date}", + "RXTZq5": "Abmeldung läuft...", + "cctOA4": "Abmelden", + "jHSHdV": "Sind Sie sicher, dass Sie sich von dieser Statusseite abmelden möchten? Sie erhalten dann keine Updates mehr.", + "47FYwb": "Abbrechen", "awr0AJ": "Monitor nicht gefunden", "CVsoUM": "Der gesuchte Monitor existiert nicht.", "BRGcS0": "Globale Latenz", @@ -29,6 +40,27 @@ "6zzIEm": "Regionale Latenz pro p75-Quantil, sortiert nach langsamster Region. Vergleichen Sie bis zu 6 Regionen.", "6pCzRs": "Gesamtverfügbarkeit", "zL23+z": "Hauptwerte der Verfügbarkeit, transparent dargestellt.", + "gjBiyj": "Wird geladen...", + "/72cxa": "Ungültiger oder abgelaufener Link", + "R10mIw": "Dieser Abmelde-Link ist nicht mehr gültig. Möglicherweise haben Sie sich bereits abgemeldet.", + "yFi/8F": "Erfolgreich abgemeldet", + "CmelO7": "Sie erhalten keine E-Mail-Benachrichtigungen mehr von {pageName}.", + "JqiqNj": "Etwas ist schiefgelaufen", + "TnvU0H": "Bitte versuchen Sie es erneut oder kontaktieren Sie den Support, wenn das Problem weiterhin besteht.", + "uW0VWi": "Von Benachrichtigungen abmelden", + "deCYKO": "Sie sind dabei, {email} von den Status-Updates von {pageName} abzumelden.", + "5HvAzP": "Bereit, Updates für {email} zu empfangen!", + "ywcUO4": "Einen Moment — wir bestätigen Ihr Abonnement", + "0Azlrb": "Verwalten", + "cyR7Kh": "Zurück", + "2yCGR2": "Link in die Zwischenablage kopiert", + "u5aHb4": "Link kopieren", + "m5BctM": "Diagrammlegende", + "csFahs": "Relativ", + "oBoa2n": "Voreinstellungen", + "gdve5D": "Benutzerdefinierter Zeitraum", + "mOFG3K": "Start", + "3JVa6k": "Ende", "79eRW1": "Bestätigung läuft...", "dX7+Rv": "Bestätigt", "m0fapd": "Bestätigung fehlgeschlagen", @@ -50,15 +82,24 @@ "xJrRMG": "Monitore", "tKMlOc": "Menü", "krEziQ": "Kontakt aufnehmen", + "8kyEWd": "Ein Quantil stellt ein bestimmtes Perzentil in Ihrem Datensatz dar.", + "kd0Igx": "Zum Beispiel ist p50 das 50. Perzentil — der Punkt, unter dem 50 % der Daten liegen. Höhere Perzentile umfassen mehr Daten und heben den oberen Bereich hervor.", "BQBZU+": "Alle Systeme betriebsbereit", "b9fOA1": "Eingeschränkte Leistung", "80EXUh": "Ausfall", "dudqv/": "Wartung", + "u++vY3": "Keine Berichte gefunden", + "2HGztY": "Keine Berichte für diese Statusseite gefunden.", + "50SA6J": "Keine öffentlichen Monitore", + "FHrzf5": "Es wurden keine öffentlichen Monitore zu dieser Seite hinzugefügt.", "2syGZB": "Bericht gelöst", + "heezSZ": "(in {duration})", + "lKJjwW": "({timeFromLast} früher)", "W6nSYE": "Gelöst", "1P6GMj": "Überwachung", "7cv4Uf": "Identifiziert", "/GKH/w": "Wird untersucht", + "VaZnIX": "(für {duration})", "FDReLp": "Keine aktuellen Benachrichtigungen", "qDj0JR": "In den letzten 7 Tagen gab es keine Berichte.", "CYs0LF": "Ereignisverlauf anzeigen", @@ -66,10 +107,14 @@ "Dnob31": "Betriebsbereit", "VQDmmK": "Eingeschränkt", "JOZGPR": "Ausfall", + "3Vaz8F": "Status-Tracker", + "apbxET": "Tag {n} Status", "GbVCQb": "Erneut klicken zum Lösen", "myq2ZL": "Normal", "KN7zKn": "Fehler", "D3rOMr": "Keine Daten", + "2wsjxR": "laufend", + "jC7BY1": "über {duration}", "uPb/gh": "Updates erhalten", "sjzDbu": "Slack", "q0qMyV": "RSS", @@ -82,7 +127,5 @@ "SyYroX": "JSON-Updates abrufen", "PSqtlY": "Status über SSH abrufen", "rptmhC": "Für Status-Updates in Slack fügen Sie den folgenden Text in einen beliebigen Kanal ein.", - "2yCGR2": "Link in die Zwischenablage kopiert", - "u5aHb4": "Link kopieren", "45YlLU": "Bestätigen Sie Ihre E-Mail-Adresse, um Updates zu erhalten, und schon sind Sie fertig." } diff --git a/apps/status-page/messages/en.json b/apps/status-page/messages/en.json index 06107aac29..1b96a3fb9e 100644 --- a/apps/status-page/messages/en.json +++ b/apps/status-page/messages/en.json @@ -17,6 +17,17 @@ "HSv9BP": "The maintenance you are looking for does not exist.", "VL1Y/1": "Report not found", "Ew1f8q": "The report you are looking for does not exist.", + "wkVkCX": "Unsubscribed successfully", + "9qFG9F": "Failed to unsubscribe", + "PV34S9": "Invalid subscription token", + "ar0fZ/": "This subscription token is no longer valid. You may have already unsubscribed or the link has expired.", + "orvpWh": "Go back", + "K8kTfz": "Manage your subscription to receive updates on the status page.", + "3JgeEq": "Unsubscribed on {date}", + "RXTZq5": "Unsubscribing...", + "cctOA4": "Unsubscribe", + "jHSHdV": "Are you sure you want to unsubscribe from this status page? You will no longer receive updates.", + "47FYwb": "Cancel", "awr0AJ": "Monitor not found", "CVsoUM": "The monitor you are looking for does not exist.", "BRGcS0": "Global Latency", @@ -29,6 +40,27 @@ "6zzIEm": "Region latency per p75 quantile, sorted by slowest region. Compare up to 6 regions.", "6pCzRs": "Total Uptime", "zL23+z": "Main values of uptime and availability, transparent.", + "gjBiyj": "Loading...", + "/72cxa": "Invalid or expired link", + "R10mIw": "This unsubscribe link is no longer valid. You may have already unsubscribed.", + "yFi/8F": "Successfully unsubscribed", + "CmelO7": "You will no longer receive email notifications from {pageName}.", + "JqiqNj": "Something went wrong", + "TnvU0H": "Please try again or contact support if the issue persists.", + "uW0VWi": "Unsubscribe from notifications", + "deCYKO": "You are about to unsubscribe {email} from {pageName} status updates.", + "5HvAzP": "All set to receive updates to {email}!", + "ywcUO4": "Hang tight - we're confirming your subscription", + "0Azlrb": "Manage", + "cyR7Kh": "Back", + "2yCGR2": "Link copied to clipboard", + "u5aHb4": "Copy Link", + "m5BctM": "Chart legend", + "csFahs": "Relative", + "oBoa2n": "Presets", + "gdve5D": "Custom Range", + "mOFG3K": "Start", + "3JVa6k": "End", "79eRW1": "Confirming...", "dX7+Rv": "Confirmed", "m0fapd": "Failed to confirm", @@ -50,15 +82,24 @@ "xJrRMG": "Monitors", "tKMlOc": "Menu", "krEziQ": "Get in touch", + "8kyEWd": "A quantile represents a specific percentile in your dataset.", + "kd0Igx": "For example, p50 is the 50th percentile - the point below which 50% of data falls. Higher percentiles include more data and highlight the upper range.", "BQBZU+": "All Systems Operational", "b9fOA1": "Degraded Performance", "80EXUh": "Downtime Performance", "dudqv/": "Maintenance", + "u++vY3": "No reports found", + "2HGztY": "No reports found for this status page.", + "50SA6J": "No public monitors", + "FHrzf5": "No public monitors have been added to this page.", "2syGZB": "Report resolved", + "heezSZ": "(in {duration})", + "lKJjwW": "({timeFromLast} earlier)", "W6nSYE": "Resolved", "1P6GMj": "Monitoring", "7cv4Uf": "Identified", "/GKH/w": "Investigating", + "VaZnIX": "(for {duration})", "FDReLp": "No recent notifications", "qDj0JR": "There have been no reports within the last 7 days.", "CYs0LF": "View events history", @@ -66,10 +107,14 @@ "Dnob31": "Operational", "VQDmmK": "Degraded", "JOZGPR": "Downtime", + "3Vaz8F": "Status tracker", + "apbxET": "Day {n} status", "GbVCQb": "Click again to unpin", "myq2ZL": "Normal", "KN7zKn": "Error", "D3rOMr": "No Data", + "2wsjxR": "ongoing", + "jC7BY1": "across {duration}", "uPb/gh": "Get updates", "sjzDbu": "Slack", "q0qMyV": "RSS", @@ -82,7 +127,5 @@ "SyYroX": "Get the JSON updates", "PSqtlY": "Get status via SSH", "rptmhC": "For status updates in Slack, paste the text below into any channel.", - "2yCGR2": "Link copied to clipboard", - "u5aHb4": "Copy Link", "45YlLU": "Validate your email to receive updates and you are all set." } diff --git a/apps/status-page/messages/fr.json b/apps/status-page/messages/fr.json index f97826335d..737b9ee9a3 100644 --- a/apps/status-page/messages/fr.json +++ b/apps/status-page/messages/fr.json @@ -17,6 +17,17 @@ "HSv9BP": "La maintenance que vous recherchez n'existe pas.", "VL1Y/1": "Rapport introuvable", "Ew1f8q": "Le rapport que vous recherchez n'existe pas.", + "wkVkCX": "Désabonnement réussi", + "9qFG9F": "Échec du désabonnement", + "PV34S9": "Jeton d'abonnement invalide", + "ar0fZ/": "Ce jeton d'abonnement n'est plus valide. Vous vous êtes peut-être déjà désabonné ou le lien a expiré.", + "orvpWh": "Retour", + "K8kTfz": "Gérez votre abonnement pour recevoir les mises à jour de la page de statut.", + "3JgeEq": "Désabonné le {date}", + "RXTZq5": "Désabonnement en cours...", + "cctOA4": "Se désabonner", + "jHSHdV": "Êtes-vous sûr de vouloir vous désabonner de cette page de statut ? Vous ne recevrez plus de mises à jour.", + "47FYwb": "Annuler", "awr0AJ": "Moniteur introuvable", "CVsoUM": "Le moniteur que vous recherchez n'existe pas.", "BRGcS0": "Latence globale", @@ -29,6 +40,27 @@ "6zzIEm": "Latence par région au quantile p75, triée par région la plus lente. Comparez jusqu'à 6 régions.", "6pCzRs": "Disponibilité totale", "zL23+z": "Valeurs principales de disponibilité, en toute transparence.", + "gjBiyj": "Chargement...", + "/72cxa": "Lien invalide ou expiré", + "R10mIw": "Ce lien de désabonnement n'est plus valide. Vous vous êtes peut-être déjà désabonné.", + "yFi/8F": "Désabonnement réussi", + "CmelO7": "Vous ne recevrez plus de notifications par email de {pageName}.", + "JqiqNj": "Une erreur est survenue", + "TnvU0H": "Veuillez réessayer ou contacter le support si le problème persiste.", + "uW0VWi": "Se désabonner des notifications", + "deCYKO": "Vous êtes sur le point de désabonner {email} des mises à jour de statut de {pageName}.", + "5HvAzP": "Prêt à recevoir les mises à jour pour {email} !", + "ywcUO4": "Patientez, nous confirmons votre abonnement", + "0Azlrb": "Gérer", + "cyR7Kh": "Retour", + "2yCGR2": "Lien copié dans le presse-papiers", + "u5aHb4": "Copier le lien", + "m5BctM": "Légende du graphique", + "csFahs": "Relatif", + "oBoa2n": "Préréglages", + "gdve5D": "Plage personnalisée", + "mOFG3K": "Début", + "3JVa6k": "Fin", "79eRW1": "Confirmation en cours...", "dX7+Rv": "Confirmé", "m0fapd": "Échec de la confirmation", @@ -50,15 +82,24 @@ "xJrRMG": "Moniteurs", "tKMlOc": "Menu", "krEziQ": "Nous contacter", + "8kyEWd": "Un quantile représente un percentile spécifique dans votre jeu de données.", + "kd0Igx": "Par exemple, p50 est le 50e percentile — le point en dessous duquel 50 % des données se situent. Les percentiles plus élevés incluent plus de données et mettent en évidence la plage supérieure.", "BQBZU+": "Tous les systèmes sont opérationnels", "b9fOA1": "Performances dégradées", "80EXUh": "Performances en panne", "dudqv/": "Maintenance", + "u++vY3": "Aucun rapport trouvé", + "2HGztY": "Aucun rapport trouvé pour cette page de statut.", + "50SA6J": "Aucun moniteur public", + "FHrzf5": "Aucun moniteur public n'a été ajouté à cette page.", "2syGZB": "Rapport résolu", + "heezSZ": "(en {duration})", + "lKJjwW": "({timeFromLast} plus tôt)", "W6nSYE": "Résolu", "1P6GMj": "Surveillance", "7cv4Uf": "Identifié", "/GKH/w": "En cours d'investigation", + "VaZnIX": "(pendant {duration})", "FDReLp": "Aucune notification récente", "qDj0JR": "Aucun rapport au cours des 7 derniers jours.", "CYs0LF": "Voir l'historique des événements", @@ -66,10 +107,14 @@ "Dnob31": "Opérationnel", "VQDmmK": "Dégradé", "JOZGPR": "En panne", + "3Vaz8F": "Suivi de statut", + "apbxET": "Statut du jour {n}", "GbVCQb": "Cliquez à nouveau pour désépingler", "myq2ZL": "Normal", "KN7zKn": "Erreur", "D3rOMr": "Aucune donnée", + "2wsjxR": "en cours", + "jC7BY1": "sur {duration}", "uPb/gh": "Recevoir les mises à jour", "sjzDbu": "Slack", "q0qMyV": "RSS", @@ -82,7 +127,5 @@ "SyYroX": "Obtenir les mises à jour JSON", "PSqtlY": "Obtenir le statut via SSH", "rptmhC": "Pour recevoir les mises à jour dans Slack, collez le texte ci-dessous dans n'importe quel canal.", - "2yCGR2": "Lien copié dans le presse-papiers", - "u5aHb4": "Copier le lien", "45YlLU": "Validez votre email pour recevoir les mises à jour et le tour est joué." } diff --git a/apps/status-page/src/app/(public)/layout.tsx b/apps/status-page/src/app/(public)/layout.tsx index 176dd3e667..b91dd433c8 100644 --- a/apps/status-page/src/app/(public)/layout.tsx +++ b/apps/status-page/src/app/(public)/layout.tsx @@ -10,6 +10,7 @@ import { SidebarProvider, } from "@openstatus/ui/components/ui/sidebar"; import { Toaster } from "@openstatus/ui/components/ui/sonner"; +import { NextIntlClientProvider } from "next-intl"; import PlausibleProvider from "next-plausible"; import { Suspense } from "react"; @@ -21,6 +22,9 @@ export default async function Layout({ }: { children: React.ReactNode; }) { + const locale = "en"; + const messages = (await import(`../../../messages/${locale}.json`)).default; + return (