Skip to content

Commit 37af771

Browse files
authored
Merge pull request #151 from stophecom/dev
Release 2026030300
2 parents 8be586c + 61d38f8 commit 37af771

72 files changed

Lines changed: 2039 additions & 1416 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ RECAPTCHA_SERVER_KEY=''
4141
PUBLIC_STRIPE_PUBLISHABLE_KEY=''
4242
STRIPE_SECRET_KEY=''
4343
STRIPE_WEBHOOK_SECRET=''
44+
45+
# Only for local development (translations)
46+
GEMINI_API_KEY=''

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ temp.*
4242
static/client-module.js
4343
# Local testing
4444
static/test.html
45+
46+
# Antigravity logs
47+
.error_log.txt

bin/cleanup-translation-keys.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { execSync } from 'node:child_process';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
5+
// Execute from the project root
6+
const projectRoot = path.join(import.meta.dirname, '..');
7+
8+
// 1. Find unused keys
9+
const enString = fs.readFileSync(path.join(projectRoot, 'messages/en.json'), 'utf-8');
10+
const en = JSON.parse(enString);
11+
const keys = Object.keys(en).filter((k) => k !== '$schema');
12+
const unusedKeys = new Set();
13+
14+
for (const key of keys) {
15+
try {
16+
execSync(`git grep -q '${key}' src/`, { cwd: projectRoot });
17+
} catch {
18+
unusedKeys.add(key);
19+
}
20+
}
21+
22+
console.log('Found ' + unusedKeys.size + ' unused keys.');
23+
24+
if (unusedKeys.size === 0) {
25+
console.log('Nothing to do.');
26+
process.exit(0);
27+
}
28+
29+
// 2. Remove them from all json files in messages/
30+
const messagesDir = path.join(projectRoot, 'messages');
31+
const files = fs.readdirSync(messagesDir).filter((f) => f.endsWith('.json'));
32+
33+
for (const file of files) {
34+
const filePath = path.join(messagesDir, file);
35+
const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
36+
let removed = 0;
37+
for (const key of unusedKeys) {
38+
if (key in content) {
39+
delete content[key];
40+
removed++;
41+
}
42+
}
43+
fs.writeFileSync(filePath, JSON.stringify(content, null, '\t') + '\n');
44+
console.log('Removed ' + removed + ' keys from ' + file);
45+
}

bin/machine-translate.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env node
2+
3+
import fs from 'fs/promises';
4+
import path from 'path';
5+
6+
// Using Gemini API (currently used AI model)
7+
// Will automatically use GEMINI_API_KEY from .env
8+
const API_KEY = process.env.GEMINI_API_KEY;
9+
10+
if (!API_KEY) {
11+
console.error('Error: Please provide GEMINI_API_KEY in your .env file to run translation.');
12+
console.error('Example: GEMINI_API_KEY=your_key npm run machine-translate');
13+
process.exit(1);
14+
}
15+
16+
const localesDir = path.join(process.cwd(), 'messages');
17+
18+
async function translateKeys(missingKeys, targetLanguage) {
19+
const prompt = `You are a professional translator. Translate the following JSON values from English to ${targetLanguage}.
20+
Keep the exact same JSON keys. Return ONLY valid JSON, no markdown formatting.
21+
22+
JSON to translate:
23+
${JSON.stringify(missingKeys, null, 2)}`;
24+
25+
const response = await fetch(
26+
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${API_KEY}`,
27+
{
28+
method: 'POST',
29+
headers: {
30+
'Content-Type': 'application/json'
31+
},
32+
body: JSON.stringify({
33+
systemInstruction: {
34+
parts: [
35+
{
36+
text: 'Output ONLY valid JSON containing the translated values for the given keys. Do not include markdown blocks like ```json.'
37+
}
38+
]
39+
},
40+
contents: [
41+
{
42+
parts: [{ text: prompt }]
43+
}
44+
],
45+
generationConfig: {
46+
responseMimeType: 'application/json'
47+
}
48+
})
49+
}
50+
);
51+
52+
if (!response.ok) {
53+
const errText = await response.text();
54+
throw new Error(`Gemini API error: ${response.status} ${response.statusText} - ${errText}`);
55+
}
56+
57+
const data = await response.json();
58+
59+
try {
60+
const textContent = data.candidates[0].content.parts[0].text;
61+
return JSON.parse(textContent);
62+
} catch (e) {
63+
console.error('Failed to parse Gemini response as JSON. Raw response:');
64+
console.error(JSON.stringify(data, null, 2));
65+
throw e;
66+
}
67+
}
68+
69+
async function main() {
70+
try {
71+
const files = await fs.readdir(localesDir);
72+
const jsonFiles = files.filter((f) => f.endsWith('.json'));
73+
74+
if (!jsonFiles.includes('en.json')) {
75+
console.error('en.json not found in messages directory!');
76+
process.exit(1);
77+
}
78+
79+
const enRaw = await fs.readFile(path.join(localesDir, 'en.json'), 'utf-8');
80+
const enData = JSON.parse(enRaw);
81+
const enKeys = Object.keys(enData);
82+
83+
for (const file of jsonFiles) {
84+
if (file === 'en.json') continue;
85+
86+
const lang = file.replace('.json', '');
87+
const targetPath = path.join(localesDir, file);
88+
const targetRaw = await fs.readFile(targetPath, 'utf-8');
89+
const targetData = JSON.parse(targetRaw);
90+
91+
const missingKeys = {};
92+
for (const key of enKeys) {
93+
if (targetData[key] === undefined || targetData[key] === null || targetData[key] === '') {
94+
missingKeys[key] = enData[key];
95+
}
96+
}
97+
98+
const missingCount = Object.keys(missingKeys).length;
99+
if (missingCount > 0) {
100+
console.log(`\nFound ${missingCount} missing keys for ${lang} (${file}). Translating...`);
101+
102+
const translated = await translateKeys(missingKeys, lang);
103+
104+
// Merge taking care of keeping the en.json sort order
105+
const finalData = {};
106+
for (const key of enKeys) {
107+
if (targetData[key] !== undefined && targetData[key] !== null && targetData[key] !== '') {
108+
finalData[key] = targetData[key];
109+
} else if (translated[key]) {
110+
finalData[key] = translated[key];
111+
} else {
112+
// Fallback to english if translation failed for a specific key unexpectedly
113+
finalData[key] = enData[key];
114+
}
115+
}
116+
117+
await fs.writeFile(targetPath, JSON.stringify(finalData, null, '\t') + '\n', 'utf-8');
118+
console.log(`✅ Successfully updated ${file}`);
119+
} else {
120+
console.log(`\n✅ ${lang} (${file}) is up to date.`);
121+
}
122+
}
123+
124+
console.log('\nAll languages checked and updated!');
125+
} catch (error) {
126+
console.error('An error occurred during translation:', error);
127+
process.exit(1);
128+
}
129+
}
130+
131+
main();

e2e/secret-text.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,14 @@ test('Add text secret ', async ({ baseURL }) => {
3434
}
3535
});
3636

37+
const responsePromise = page.waitForResponse((response) =>
38+
response.url().includes('?/postSecret')
39+
);
40+
await expect(page.getByTestId('secret-form-submit')).toBeEnabled();
3741
await page.getByTestId('secret-form-submit').click();
42+
await responsePromise;
3843

39-
await expect(page.getByTestId('copy-link')).toBeVisible();
44+
await expect(page.getByTestId('copy-link')).toBeVisible({ timeout: 15000 });
4045
await page.getByTestId('copy-link').click();
4146
secretUrl = await page.evaluate(() => navigator.clipboard.readText());
4247

0 commit comments

Comments
 (0)