Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Built by [@ctala](https://github.com/ctala) | 🌐 [cristiantala.com](https://cristiantala.com)

![Version](https://img.shields.io/badge/version-1.2.3-22c55e)
![Version](https://img.shields.io/badge/version-1.3.0-22c55e)
![Manifest](https://img.shields.io/badge/manifest-v3-3b82f6)
![License](https://img.shields.io/badge/license-MIT-94a3b8)

Expand Down Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
@@ -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?"
}
}
59 changes: 59 additions & 0 deletions _locales/es/messages.json
Original file line number Diff line number Diff line change
@@ -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?"
}
}
7 changes: 4 additions & 3 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
27 changes: 14 additions & 13 deletions popup.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="es">
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
Expand Down Expand Up @@ -229,46 +229,47 @@
</head>
<body>
<div class="header">
<h1>🔬 API Reverse Engineer</h1>
<div class="subtitle">Captura requests en cualquier sitio</div>
<h1>🔬 <span data-i18n="popupTitle">API Reverse Engineer</span></h1>
<div class="subtitle" data-i18n="popupSubtitle">Capture requests on any site</div>
</div>

<div class="stats">
<div class="stat">
<div class="stat-value" id="totalCount">0</div>
<div class="stat-label">Requests</div>
<div class="stat-label" data-i18n="statRequests">Requests</div>
</div>
<div class="stat">
<div class="stat-value" id="uniqueCount">0</div>
<div class="stat-label">Únicos</div>
<div class="stat-label" data-i18n="statUnique">Unique</div>
</div>
</div>

<div class="filter-section">
<label>Filtrar por URL (opcional)</label>
<label data-i18n="filterLabel">Filter by URL (optional)</label>
<input
type="text"
id="filterInput"
class="filter-input"
placeholder="ej: api2.skool.com, /api/v1, graphql"
data-i18n-placeholder="filterPlaceholder"
placeholder="e.g. api2.skool.com, /api/v1, graphql"
/>
</div>

<div class="actions">
<button id="btnRecord">▶ Iniciar</button>
<button id="btnDownload" disabled>⬇ Descargar JSON</button>
<button id="btnClear" title="Limpiar">🗑</button>
<button id="btnRecord" data-i18n="btnStart">▶ Start</button>
<button id="btnDownload" disabled data-i18n="btnDownload">⬇ Download JSON</button>
<button id="btnClear" data-i18n-title="btnClearTitle" title="Clear">🗑</button>
</div>

<div class="recording-indicator" id="recordingIndicator">
<div class="pulse"></div>
Grabando...
<span data-i18n="recordingIndicator">Recording...</span>
</div>

<div class="preview" id="preview">
<div class="preview-title">Endpoints capturados</div>
<div class="preview-title" data-i18n="endpointsCapturedTitle">Captured endpoints</div>
<div id="endpointList">
<div class="empty-state">Presiona Iniciar y usa el sitio normalmente</div>
<div class="empty-state" data-i18n="emptyState">Press Start and use the site normally</div>
</div>
</div>

Expand Down
54 changes: 39 additions & 15 deletions src/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
}
Expand All @@ -46,7 +69,7 @@ function updateUI(total, unique) {

function renderEndpoints(endpoints) {
if (!endpoints || endpoints.length === 0) {
endpointList.innerHTML = '<div class="empty-state">Presiona Iniciar y usa el sitio normalmente</div>';
endpointList.innerHTML = `<div class="empty-state">${chrome.i18n.getMessage('emptyState')}</div>`;
return;
}

Expand Down Expand Up @@ -76,21 +99,21 @@ 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;

chrome.runtime.sendMessage({ type: 'START', filter, tabId }, () => {
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 = `<div class="empty-state">Grabando en <strong style="color:#22c55e">${hostname}</strong><br>Usa el sitio normalmente</div>`;
// 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 = `<div class="empty-state">${chrome.i18n.getMessage('recordingOnTab')} <strong style="color:#22c55e">${hostname}</strong><br>${chrome.i18n.getMessage('useTheSiteNormally')}</div>`;
});
} else {
chrome.runtime.sendMessage({ type: 'STOP' }, () => {
Expand All @@ -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';
Expand All @@ -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 = '<div class="empty-state">Presiona Iniciar y usa el sitio normalmente</div>';
endpointList.innerHTML = `<div class="empty-state">${chrome.i18n.getMessage('emptyState')}</div>`;
});
});

// Auto-refresh mientras está grabando
// Auto-refresh while recording
setInterval(() => {
if (isRecording) refreshPreview();
}, 1500);

// Iniciar
// Start
applyI18n();
loadState();