diff --git a/package.json b/package.json
index 45f75bb0d..068bc4351 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"update-db": "prisma migrate deploy",
"check-db": "node scripts/check-db.js",
"check-env": "node scripts/check-env.js",
+ "check-missing-messages": "node scripts/check-missing-messages.js",
"copy-db-files": "node scripts/copy-db-files.js",
"download-country-names": "node scripts/download-country-names.js",
"download-language-names": "node scripts/download-language-names.js",
diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json
index 969f9537c..66d837cb5 100644
--- a/public/intl/messages/en-US.json
+++ b/public/intl/messages/en-US.json
@@ -250,6 +250,7 @@
"remaining": "Remaining",
"remove": "Remove",
"remove-member": "Remove member",
+ "play": "Play",
"replay": "Replay",
"replay-id": "Replay ID",
"replay-enabled": "Replay enabled",
diff --git a/scripts/check-missing-messages.js b/scripts/check-missing-messages.js
new file mode 100644
index 000000000..3e497abd2
--- /dev/null
+++ b/scripts/check-missing-messages.js
@@ -0,0 +1,60 @@
+import { readdirSync, readFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+const messagesDir = join(__dirname, '..', 'public', 'intl', 'messages');
+
+const en = JSON.parse(readFileSync(join(messagesDir, 'en-US.json'), 'utf8'));
+
+// Flatten nested structure: { label: { foo: 'bar' } } -> { 'label.foo': 'bar' }
+function flatten(obj, prefix = '') {
+ const result = {};
+ for (const [k, v] of Object.entries(obj)) {
+ const key = prefix ? `${prefix}.${k}` : k;
+ if (typeof v === 'object' && v !== null) {
+ Object.assign(result, flatten(v, key));
+ } else {
+ result[key] = v;
+ }
+ }
+ return result;
+}
+
+const enFlat = flatten(en);
+const enKeys = Object.keys(enFlat);
+console.log(`en-US.json has ${enKeys.length} keys`);
+
+const files = readdirSync(messagesDir)
+ .filter(f => f.endsWith('.json') && f !== 'en-US.json')
+ .sort();
+
+const allMissing = {};
+let total = 0;
+
+for (const fname of files) {
+ const data = JSON.parse(readFileSync(join(messagesDir, fname), 'utf8'));
+ const flat = flatten(data);
+ const missing = enKeys.filter(k => !(k in flat));
+ if (missing.length) {
+ allMissing[fname] = missing;
+ console.log(`${fname}: ${missing.length} missing`);
+ total += missing.length;
+ }
+}
+console.log(`\nTotal missing across all locales: ${total}`);
+
+const keyCounts = {};
+for (const missing of Object.values(allMissing)) {
+ for (const k of missing) {
+ keyCounts[k] = (keyCounts[k] || 0) + 1;
+ }
+}
+const sorted = Object.entries(keyCounts).sort((a, b) => b[1] - a[1]);
+if (sorted.length) {
+ console.log('\nMost commonly missing keys:');
+ for (const [k, count] of sorted) {
+ console.log(` "${k}": missing from ${count} files (en value: "${enFlat[k]}")`);
+ }
+}
diff --git a/src/app/(main)/websites/[websiteId]/replays/ReplaysTable.tsx b/src/app/(main)/websites/[websiteId]/replays/ReplaysTable.tsx
index 3a39cea1e..ce57c8538 100644
--- a/src/app/(main)/websites/[websiteId]/replays/ReplaysTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/replays/ReplaysTable.tsx
@@ -20,6 +20,15 @@ export function ReplaysTable({ ...props }: DataTableProps) {
return (
+
+ {(row: any) => (
+
+ )}
+
{(row: any) => (
@@ -63,15 +72,6 @@ export function ReplaysTable({ ...props }: DataTableProps) {
{(row: any) => }
-
- {(row: any) => (
-
- )}
-
);
}
diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionReplaysTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionReplaysTable.tsx
index a2cf3ef7b..ff481f550 100644
--- a/src/app/(main)/websites/[websiteId]/sessions/SessionReplaysTable.tsx
+++ b/src/app/(main)/websites/[websiteId]/sessions/SessionReplaysTable.tsx
@@ -19,14 +19,6 @@ export function SessionReplaysTable({
return (
-
-
- {(row: any) => formatDuration(row.duration || 0)}
-
-
-
- {(row: any) => }
-
{(row: any) => (
+
+
+ {(row: any) => formatDuration(row.duration || 0)}
+
+
+
+ {(row: any) => }
+
);
}
diff --git a/src/app/(main)/websites/[websiteId]/settings/WebsiteReplaySettings.tsx b/src/app/(main)/websites/[websiteId]/settings/WebsiteReplaySettings.tsx
index 042d8480d..d88923d78 100644
--- a/src/app/(main)/websites/[websiteId]/settings/WebsiteReplaySettings.tsx
+++ b/src/app/(main)/websites/[websiteId]/settings/WebsiteReplaySettings.tsx
@@ -32,6 +32,28 @@ export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
const [maxDuration, setMaxDuration] = useState(String(config.maxDuration ?? 300000));
const [blockSelector, setBlockSelector] = useState(config.blockSelector ?? '');
+ const handleToggle = async (value: boolean) => {
+ const previous = enabled;
+ setEnabled(value);
+
+ try {
+ await mutateAsync(
+ {
+ replayEnabled: value,
+ },
+ {
+ onSuccess: async () => {
+ toast(t(messages.saved));
+ touch('websites');
+ touch(`website:${websiteId}`);
+ },
+ },
+ );
+ } catch {
+ setEnabled(previous);
+ }
+ };
+
const handleSave = async () => {
await mutateAsync(
{
@@ -47,7 +69,7 @@ export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
onSuccess: async () => {
toast(t(messages.saved));
touch('websites');
- touch(`website:${website.id}`);
+ touch(`website:${websiteId}`);
},
},
);
@@ -56,7 +78,7 @@ export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
return (
-
+
{t(labels.replayEnabled)}
{enabled && (
@@ -92,13 +114,13 @@ export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
+
+
+
>
)}
-
-
-
);
}
diff --git a/src/components/messages.ts b/src/components/messages.ts
index 11899a5c3..867623231 100644
--- a/src/components/messages.ts
+++ b/src/components/messages.ts
@@ -358,6 +358,7 @@ export const labels: Record = {
needsImprovement: 'label.needs-improvement',
poor: 'label.poor',
sampleSize: 'label.sample-size',
+ play: 'label.play',
replays: 'label.replays',
replay: 'label.replay',
replayId: 'label.replay-id',