diff --git a/README.md b/README.md
index 37d93f7..3db6d05 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
Built by [@ctala](https://github.com/ctala) | 🌐 [cristiantala.com](https://cristiantala.com)
-
+


@@ -56,6 +56,12 @@ Get the extension directly from the Chrome Web Store:
## Changelog
+### v1.3.0 (2026-06-19)
+**Added:**
+- English locale — popup UI now follows `chrome.i18n` and the browser's language, with English and Spanish supported out of the box
+- `_locales/en` and `_locales/es` message catalogs
+- `default_locale` set in `manifest.json`
+
### v1.2.3 (2026-02-20)
**Fixed:**
- CSP bypass for ultra-strict sites (Skool, etc.) — now uses `chrome.scripting.executeScript` with `world: 'MAIN'` instead of DOM script injection
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
new file mode 100644
index 0000000..4bb01e8
--- /dev/null
+++ b/_locales/en/messages.json
@@ -0,0 +1,59 @@
+{
+ "extName": {
+ "message": "API Reverse Engineer"
+ },
+ "extDescription": {
+ "message": "Capture every API call as you browse. Record → use the site → download JSON."
+ },
+ "popupTitle": {
+ "message": "API Reverse Engineer"
+ },
+ "popupSubtitle": {
+ "message": "Capture requests on any site"
+ },
+ "statRequests": {
+ "message": "Requests"
+ },
+ "statUnique": {
+ "message": "Unique"
+ },
+ "filterLabel": {
+ "message": "Filter by URL (optional)"
+ },
+ "filterPlaceholder": {
+ "message": "e.g. api2.skool.com, /api/v1, graphql"
+ },
+ "btnStart": {
+ "message": "▶ Start"
+ },
+ "btnStop": {
+ "message": "⏹ Stop"
+ },
+ "btnDownload": {
+ "message": "⬇ Download JSON"
+ },
+ "btnClearTitle": {
+ "message": "Clear"
+ },
+ "recordingIndicator": {
+ "message": "Recording..."
+ },
+ "endpointsCapturedTitle": {
+ "message": "Captured endpoints"
+ },
+ "emptyState": {
+ "message": "Press Start and use the site normally"
+ },
+ "recordingOnTab": {
+ "message": "Recording on"
+ },
+ "useTheSiteNormally": {
+ "message": "Use the site normally"
+ },
+ "currentTabFallback": {
+ "message": "current tab"
+ },
+ "confirmClearData": {
+ "message": "Clear all captured data?"
+ }
+}
diff --git a/_locales/es/messages.json b/_locales/es/messages.json
new file mode 100644
index 0000000..40d4707
--- /dev/null
+++ b/_locales/es/messages.json
@@ -0,0 +1,59 @@
+{
+ "extName": {
+ "message": "API Reverse Engineer"
+ },
+ "extDescription": {
+ "message": "Captura todas las llamadas API mientras navegas. Record → usa el sitio → descarga JSON."
+ },
+ "popupTitle": {
+ "message": "API Reverse Engineer"
+ },
+ "popupSubtitle": {
+ "message": "Captura requests en cualquier sitio"
+ },
+ "statRequests": {
+ "message": "Requests"
+ },
+ "statUnique": {
+ "message": "Únicos"
+ },
+ "filterLabel": {
+ "message": "Filtrar por URL (opcional)"
+ },
+ "filterPlaceholder": {
+ "message": "ej: api2.skool.com, /api/v1, graphql"
+ },
+ "btnStart": {
+ "message": "▶ Iniciar"
+ },
+ "btnStop": {
+ "message": "⏹ Detener"
+ },
+ "btnDownload": {
+ "message": "⬇ Descargar JSON"
+ },
+ "btnClearTitle": {
+ "message": "Limpiar"
+ },
+ "recordingIndicator": {
+ "message": "Grabando..."
+ },
+ "endpointsCapturedTitle": {
+ "message": "Endpoints capturados"
+ },
+ "emptyState": {
+ "message": "Presiona Iniciar y usa el sitio normalmente"
+ },
+ "recordingOnTab": {
+ "message": "Grabando en"
+ },
+ "useTheSiteNormally": {
+ "message": "Usa el sitio normalmente"
+ },
+ "currentTabFallback": {
+ "message": "tab actual"
+ },
+ "confirmClearData": {
+ "message": "¿Limpiar todos los datos capturados?"
+ }
+}
diff --git a/manifest.json b/manifest.json
index 090c9b2..e2dd181 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,8 +1,9 @@
{
"manifest_version": 3,
- "name": "API Reverse Engineer",
- "version": "1.2.3",
- "description": "Captura todas las llamadas API mientras navegas. Record \u2192 usa el sitio \u2192 descarga JSON.",
+ "name": "__MSG_extName__",
+ "version": "1.3.0",
+ "description": "__MSG_extDescription__",
+ "default_locale": "en",
"permissions": [
"storage",
"activeTab",
diff --git a/popup.html b/popup.html
index 06910e2..1e181e0 100644
--- a/popup.html
+++ b/popup.html
@@ -1,5 +1,5 @@
-
+
@@ -229,46 +229,47 @@
-
+
-
-
-
+
+
+
- Grabando...
+
Recording...
-
Endpoints capturados
+
Captured endpoints
-
Presiona Iniciar y usa el sitio normalmente
+
Press Start and use the site normally
diff --git a/src/popup.js b/src/popup.js
index 3d0db3e..ce703ff 100644
--- a/src/popup.js
+++ b/src/popup.js
@@ -13,7 +13,30 @@ const recordingIndicator = document.getElementById('recordingIndicator');
let isRecording = false;
-// Cargar estado al abrir popup
+// Populate all [data-i18n] elements with the active locale's strings
+function applyI18n() {
+ document.querySelectorAll('[data-i18n]').forEach((el) => {
+ const key = el.getAttribute('data-i18n');
+ const msg = chrome.i18n.getMessage(key);
+ if (msg) el.textContent = msg;
+ });
+
+ document.querySelectorAll('[data-i18n-placeholder]').forEach((el) => {
+ const key = el.getAttribute('data-i18n-placeholder');
+ const msg = chrome.i18n.getMessage(key);
+ if (msg) el.setAttribute('placeholder', msg);
+ });
+
+ document.querySelectorAll('[data-i18n-title]').forEach((el) => {
+ const key = el.getAttribute('data-i18n-title');
+ const msg = chrome.i18n.getMessage(key);
+ if (msg) el.setAttribute('title', msg);
+ });
+
+ document.title = chrome.i18n.getMessage('popupTitle') || document.title;
+}
+
+// Load state on popup open
function loadState() {
chrome.runtime.sendMessage({ type: 'GET_STATE' }, (res) => {
if (!res) return;
@@ -32,11 +55,11 @@ function updateUI(total, unique) {
uniqueCount.textContent = unique || 0;
if (isRecording) {
- btnRecord.textContent = '⏹ Detener';
+ btnRecord.textContent = chrome.i18n.getMessage('btnStop');
btnRecord.classList.add('recording');
recordingIndicator.classList.add('active');
} else {
- btnRecord.textContent = '▶ Iniciar';
+ btnRecord.textContent = chrome.i18n.getMessage('btnStart');
btnRecord.classList.remove('recording');
recordingIndicator.classList.remove('active');
}
@@ -46,7 +69,7 @@ function updateUI(total, unique) {
function renderEndpoints(endpoints) {
if (!endpoints || endpoints.length === 0) {
- endpointList.innerHTML = 'Presiona Iniciar y usa el sitio normalmente
';
+ endpointList.innerHTML = `${chrome.i18n.getMessage('emptyState')}
`;
return;
}
@@ -76,11 +99,11 @@ function refreshPreview() {
});
}
-// Botón Record / Stop
+// Record / Stop button
btnRecord.addEventListener('click', async () => {
if (!isRecording) {
const filter = filterInput.value.trim();
- // Obtener el tab activo para grabar solo en él
+ // Get the active tab so we only record on it
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const tabId = tab?.id || null;
@@ -88,9 +111,9 @@ btnRecord.addEventListener('click', async () => {
isRecording = true;
chrome.storage.local.set({ filter });
updateUI(0, 0);
- // Mostrar en qué tab está grabando
- const hostname = tab?.url ? (() => { try { return new URL(tab.url).hostname; } catch { return tab.url; } })() : 'tab actual';
- endpointList.innerHTML = `Grabando en ${hostname}
Usa el sitio normalmente
`;
+ // Show which tab it's recording on
+ const hostname = tab?.url ? (() => { try { return new URL(tab.url).hostname; } catch { return tab.url; } })() : chrome.i18n.getMessage('currentTabFallback');
+ endpointList.innerHTML = `${chrome.i18n.getMessage('recordingOnTab')} ${hostname}
${chrome.i18n.getMessage('useTheSiteNormally')}
`;
});
} else {
chrome.runtime.sendMessage({ type: 'STOP' }, () => {
@@ -100,7 +123,7 @@ btnRecord.addEventListener('click', async () => {
}
});
-// Botón Descargar
+// Download button
btnDownload.addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const site = tab?.url ? new URL(tab.url).hostname : 'unknown';
@@ -118,19 +141,20 @@ btnDownload.addEventListener('click', async () => {
});
});
-// Botón Limpiar
+// Clear button
btnClear.addEventListener('click', () => {
- if (!confirm('¿Limpiar todos los datos capturados?')) return;
+ if (!confirm(chrome.i18n.getMessage('confirmClearData'))) return;
chrome.runtime.sendMessage({ type: 'CLEAR' }, () => {
updateUI(0, 0);
- endpointList.innerHTML = 'Presiona Iniciar y usa el sitio normalmente
';
+ endpointList.innerHTML = `${chrome.i18n.getMessage('emptyState')}
`;
});
});
-// Auto-refresh mientras está grabando
+// Auto-refresh while recording
setInterval(() => {
if (isRecording) refreshPreview();
}, 1500);
-// Iniciar
+// Start
+applyI18n();
loadState();