diff --git a/apps/status-page/messages/de.json b/apps/status-page/messages/de.json new file mode 100644 index 0000000000..91c6836b11 --- /dev/null +++ b/apps/status-page/messages/de.json @@ -0,0 +1,90 @@ +{ + "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/en.json b/apps/status-page/messages/en.json new file mode 100644 index 0000000000..5a5d5c0ed2 --- /dev/null +++ b/apps/status-page/messages/en.json @@ -0,0 +1,90 @@ +{ + "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", + "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", + "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 new file mode 100644 index 0000000000..91c6836b11 --- /dev/null +++ b/apps/status-page/messages/es.json @@ -0,0 +1,90 @@ +{ + "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 new file mode 100644 index 0000000000..91c6836b11 --- /dev/null +++ b/apps/status-page/messages/fr.json @@ -0,0 +1,90 @@ +{ + "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/ja.json b/apps/status-page/messages/ja.json new file mode 100644 index 0000000000..91c6836b11 --- /dev/null +++ b/apps/status-page/messages/ja.json @@ -0,0 +1,90 @@ +{ + "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 new file mode 100644 index 0000000000..91c6836b11 --- /dev/null +++ b/apps/status-page/messages/ko.json @@ -0,0 +1,90 @@ +{ + "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 new file mode 100644 index 0000000000..91c6836b11 --- /dev/null +++ b/apps/status-page/messages/pt.json @@ -0,0 +1,90 @@ +{ + "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 new file mode 100644 index 0000000000..91c6836b11 --- /dev/null +++ b/apps/status-page/messages/zh.json @@ -0,0 +1,90 @@ +{ + "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 9ff1620b62..c73949080d 100644 --- a/apps/status-page/next.config.ts +++ b/apps/status-page/next.config.ts @@ -1,6 +1,21 @@ import { withSentryConfig } from "@sentry/nextjs"; - import type { NextConfig } from "next"; +import createNextIntlPlugin from "next-intl/plugin"; + +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, @@ -21,6 +36,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).*)", @@ -48,7 +92,7 @@ const nextConfig: NextConfig = { : "localhost", }, ], - destination: "/:subdomain/:path*", + destination: "/:subdomain/en/:path*", }, ], }; @@ -79,4 +123,4 @@ const sentryConfig = { }, }; -export default withSentryConfig(nextConfig, sentryConfig); +export default withSentryConfig(withNextIntl(nextConfig), sentryConfig); 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/app/(public)/client.tsx b/apps/status-page/src/app/(public)/client.tsx index a11495dc46..175b55fdf5 100644 --- a/apps/status-page/src/app/(public)/client.tsx +++ b/apps/status-page/src/app/(public)/client.tsx @@ -10,6 +10,7 @@ import { SectionTitle, } from "@/components/content/section"; import { recomputeStyles } from "@/components/status-page/floating-button"; +import { StatusMonitorStatic } from "@/components/status-page/static/status-monitor-static"; import { Status, StatusContent, @@ -17,13 +18,12 @@ 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 { ThemePalettePicker } from "@/components/themes/theme-palette-picker"; 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"; @@ -298,7 +298,7 @@ function ThemePlaygroundStatus({ {/* TODO: create mock data */} - (); const [state, setState] = useState<"idle" | "pending" | "success">("idle"); @@ -54,10 +56,11 @@ 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 +71,7 @@ export function SectionMagicLink() { form="email-form" disabled={state === "pending"} > - {state === "pending" ? "Submitting..." : "Submit"} + {state === "pending" ? t("Submitting...") : t("Submit")} ) : ( @@ -79,12 +82,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]/(auth)/login/_components/section-password.tsx b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/_components/section-password.tsx similarity index 88% 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 index e42c6ce547..69b166c7e6 100644 --- 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 @@ -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]/(auth)/login/actions.ts b/apps/status-page/src/app/(status-page)/[domain]/[locale]/(auth)/login/actions.ts similarity index 89% 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 index 57c1e32c28..80834c1d1a 100644 --- 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 @@ -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/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 94% 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 index 1b716087a7..40bf667533 100644 --- 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 @@ -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]/(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 90% 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 index 20cb726716..55dd272d9b 100644 --- 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 @@ -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]/(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 93% 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 index 1cd49cd63a..2d8c00b200 100644 --- 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 @@ -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]/(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 89% 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 index 37b518c005..6e6fe4c351 100644 --- 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 @@ -14,7 +14,6 @@ import { ChartLineRegions, ChartLineRegionsSkeleton, } from "@/components/chart/chart-line-regions"; -import { PopoverQuantile } from "@/components/popover/popover-quantile"; import { Status, StatusContent, @@ -47,12 +46,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 +206,8 @@ export default function Page() { if (!isLoading && !monitor) { return ( ); } @@ -231,7 +232,7 @@ export default function Page() { - Global Latency + {t("Global Latency")} {isLoading ? ( @@ -246,13 +247,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 +273,7 @@ export default function Page() { {uptimePercentage}{" "} - {totalChecks} checks + {totalChecks} {t("checks")} )} @@ -281,10 +282,11 @@ 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 +306,11 @@ 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 +327,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 ? ( 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..4e0f39fffb --- /dev/null +++ b/apps/status-page/src/app/(status-page)/[domain]/[locale]/layout.tsx @@ -0,0 +1,29 @@ +import { routing } from "@/i18n/routing"; +import { NextIntlClientProvider, hasLocale } from "next-intl"; +import { setRequestLocale } from "next-intl/server"; +import { notFound } from "next/navigation"; + +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}.json`)) + .default; + + return ( + + {children} + + ); +} 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/language-switcher.tsx b/apps/status-page/src/components/language-switcher.tsx new file mode 100644 index 0000000000..2b2a1efd59 --- /dev/null +++ b/apps/status-page/src/components/language-switcher.tsx @@ -0,0 +1,84 @@ +"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/nav/footer.tsx b/apps/status-page/src/components/nav/footer.tsx index 8f390ef7e5..0370ab2219 100644 --- a/apps/status-page/src/components/nav/footer.tsx +++ b/apps/status-page/src/components/nav/footer.tsx @@ -2,15 +2,18 @@ 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"; 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 +34,7 @@ export function Footer(props: React.ComponentProps<"footer">) {
{!page.whiteLabel ? (

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

diff --git a/apps/status-page/src/components/nav/header.tsx b/apps/status-page/src/components/nav/header.tsx index fbcbbacefe..e9e40ffa28 100644 --- a/apps/status-page/src/components/nav/header.tsx +++ b/apps/status-page/src/components/nav/header.tsx @@ -26,6 +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 } from "next-intl"; import NextLink from "next/link"; import { useParams, usePathname } from "next/navigation"; import { useState } from "react"; @@ -34,22 +35,23 @@ import { toast } from "sonner"; type Page = RouterOutputs["statusPage"]["get"]; function useNav() { + const t = useExtracted(); const pathname = usePathname(); const prefix = usePathnamePrefix(); return [ { - label: "Status", + label: t("Status"), href: `/${prefix}`, isActive: pathname === `/${prefix}`, }, { - label: "Events", + label: t("Events"), href: `${prefix ? `/${prefix}` : ""}/events`, isActive: pathname.startsWith(`${prefix ? `/${prefix}` : ""}/events`), }, { - label: "Monitors", + label: t("Monitors"), href: `${prefix ? `/${prefix}` : ""}/monitors`, isActive: pathname.startsWith(`${prefix ? `/${prefix}` : ""}/monitors`), }, @@ -72,6 +74,7 @@ function getStatusUpdateTypes(page: Page): StatusUpdateType[] { } export function Header(props: React.ComponentProps<"header">) { + 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")}

    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/static/status-monitor-static.tsx b/apps/status-page/src/components/status-page/static/status-monitor-static.tsx new file mode 100644 index 0000000000..f1a3a136b8 --- /dev/null +++ b/apps/status-page/src/components/status-page/static/status-monitor-static.tsx @@ -0,0 +1,165 @@ +"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"; +import { + AlertCircleIcon, + CheckIcon, + TriangleAlertIcon, + WrenchIcon, +} from "lucide-react"; +import { + StatusMonitorDescription, + StatusMonitorTitle, + StatusMonitorUptime, + StatusMonitorUptimeSkeleton, +} from "../status-monitor"; + +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/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..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,9 +1,13 @@ 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 { + 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, @@ -13,7 +17,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 +84,7 @@ export function StatusEventTitleCheck({ children, ...props }: React.ComponentProps<"div">) { + const t = useExtracted(); return (
    @@ -90,7 +95,7 @@ export function StatusEventTitleCheck({
    -

    Report resolved

    +

    {t("Report resolved")}

    @@ -186,11 +191,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; @@ -249,6 +252,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 +274,7 @@ export function StatusEventTimelineReportUpdate({ ) : null}
    - {status[report.status]}{" "} + {statusLabels[report.status]}{" "} ·{" "} @@ -302,6 +313,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 +332,7 @@ export function StatusEventTimelineMaintenance({ {/* NOTE: is always last, no need for className="mb-2" */}
    - Maintenance{" "} + {t("Maintenance")}{" "} ·{" "} @@ -350,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 ( - - ); -} 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")}
    ); 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..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,9 +14,9 @@ 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 { useExtracted } from "next-intl"; import Link from "next/link"; 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} 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..225d292a11 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,9 @@ 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 +123,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 +196,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 +208,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.")}

    ); 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; } diff --git a/apps/status-page/src/i18n/config.ts b/apps/status-page/src/i18n/config.ts new file mode 100644 index 0000000000..ad12ac8044 --- /dev/null +++ b/apps/status-page/src/i18n/config.ts @@ -0,0 +1,3 @@ +export const locales = ["en", "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 new file mode 100644 index 0000000000..b59fbdd409 --- /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 as readonly string[]).includes(locale)) { + locale = routing.defaultLocale; + } + + return { + locale, + messages: (await import(`../../messages/${locale}.json`)).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..44721fe073 --- /dev/null +++ b/apps/status-page/src/i18n/routing.ts @@ -0,0 +1,8 @@ +import { defineRouting } from "next-intl/routing"; +import { defaultLocale, locales } from "./config"; + +export const routing = defineRouting({ + locales, + defaultLocale, + 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); 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: