Cardiology Translator with Claude
Build a bidirectional translator that helps cardiologists and patients communicate across languages.
Introduction
30 Second Summary
Imagine a patient arrives at a cardiology office for a post-procedure follow-up. The cardiologist needs to explain "Your ejection fraction is 35%, so we're initiating ACE inhibitor therapy and scheduling a cardiac catheterization." Even a direct translation into the patient's language keeps the confusing cardiology jargon intact.
In this project, you will build a bidirectional cardiology translator powered by Claude that does something standard translation tools cannot: it simultaneously translates AND simplifies cardiology terminology into plain language, and it works both ways so the cardiologist and the patient's family can communicate back and forth.
What You'll Build
A cardiologist types a complex instruction, picks a language, and instantly sees a patient-friendly version. The patient's family types back in their language, and the cardiologist hears the English translation spoken aloud right from the browser.
By the end of this project, you'll have:
- A working bidirectional translator where the cardiologist types instructions in English and the patient's family sees them in their language with all cardiology jargon replaced by simple words.
- A direction toggle that lets the patient or family type back in their language so the cardiologist can read and hear the English translation spoken aloud through the browser.
- A quick-phrase panel with common cardiology instructions (blood thinner reminders, heart catheterization prep, low-sodium diet, when to call 911) that translate with a single click.
- Hands-on experience with prompt engineering, where you watch a naive translation prompt fail on cardiology terms and then fix it by designing a specialty-specific system prompt that produces dramatically better results.
- Secret Mission: Add voice input so the patient can speak directly into the browser microphone instead of typing.
Are there any prerequisites?
You should be comfortable with basic HTML, CSS, and JavaScript. You will also need a free Anthropic account to get an API key for Claude.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
Picture this: a patient and their family arrive at a cardiology office after an echocardiogram. The cardiologist needs to explain that the patient's heart is pumping at only 35% capacity and they need to start new medication. A tool like Google Translate would convert the words, but it keeps confusing cardiology terms intact. The family still has no idea what "ejection fraction" or "ACE inhibitor" means, even in their own language.
Set Up the Project
Before you can build the translator, you need a project structure and the right tools installed. Express will serve your web pages and handle API requests from the browser.
The Anthropic SDK will let your server call Claude for translations. In this step, you'll create the project folder, install both packages, and stand up a basic server that proves everything is wired correctly.
In this step, get ready to:
- Create a Node.js project with Express and the Anthropic SDK installed.
- Build a minimal server that serves static files.
- Confirm the server runs and displays a page in the browser.
Create the project folder and install dependencies
Every Node.js project starts with a folder and a package.json file that tracks your dependencies. You'll create both now, then install the two packages this project needs.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (backtick key, same on both macOS and Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir clinic-translator
cd clinic-translator
- Initialize a new Node.js project by running:
npm init -y
You should see a block of JSON printed in your terminal showing the new package.json contents.
- Enable ES module syntax and install your two dependencies by running these commands:
npm pkg set type=module
npm install express @anthropic-ai/sdk
You should see output ending with something like "added 19 packages" (the exact number may vary).
What did these commands do?
npm pkg set type=module tells Node.js to treat your .js files as ES modules, so you can use import statements instead of require().
npm install express @anthropic-ai/sdk downloads Express (the web server framework) and the Anthropic SDK (for calling Claude) into a node_modules folder and records them in your package.json.
🙋♀️ Seeing permission errors or command not found?
If npm is not recognized, Node.js may not be on your system PATH. Close and reopen VS Code, then try again.
On macOS, if you see "EACCES" permission errors, do not use sudo. Instead, reinstall Node.js using the official installer from nodejs.org.
help me fix npm permission errors or command not found issues
Create the server and verify it starts
Now you'll write a minimal Express server that serves files from a public folder. This is the backbone your translator will run on.
- Create the public folder by running:
mkdir public
- In VS Code's Explorer sidebar (left panel), click the New File icon at the top of the file tree.
- Name the file server.js and press Enter.
- Paste the following code into server.js:
import express from 'express';
// Create the Express app
const app = express();
const port = 3000;
// Parse JSON request bodies
app.use(express.json());
// Serve static files from the 'public' folder
app.use(express.static('public'));
// Start the server
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
What does this code do?
express.json() lets your server understand JSON data sent from the browser (you'll need this when the frontend sends translation requests later).
express.static('public') tells Express to serve any files inside the public folder (HTML, CSS, JavaScript) directly to the browser when someone visits your URL.
- Save server.js with Cmd+S (macOS) or Ctrl+S (Windows).
- Start the server by running this in your terminal:
node server.js
You should see this message printed in your terminal:
Server running on http://localhost:3000
Server not starting?
If you see "Cannot find module express", make sure you ran npm install express @anthropic-ai/sdk from inside the clinic-translator folder.
If you see "SyntaxError: Cannot use import statement", confirm your package.json contains "type": "module". Run npm pkg set type=module again if needed.
help me fix my Express server startup error
- Stop the server by pressing Ctrl+C in the terminal.
Add the HTML page and confirm it loads in the browser
The server is running but has nothing to show yet. You'll create a simple HTML page so you can see the app in a browser.
- In VS Code's Explorer sidebar, right-click the public folder and select New File.
- Name it index.html and press Enter.
- Paste the following code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cardiology Translator</title>
</head>
<body>
<h1>Cardiology Translator</h1>
</body>
</html>
What does this page do?
This is a bare-bones HTML page with just a heading. Express will serve this file automatically when someone visits http://localhost:3000 because it looks for index.html inside the public folder by default.
- Save index.html with Cmd+S (macOS) or Ctrl+S (Windows).
Before you start the server again, what do you think you'll see when you open http://localhost:3000 in your browser?
- Start the server again by running:
node server.js
- Open your web browser and navigate to http://localhost:3000.
You should see a page with the heading "Cardiology Translator" displayed in large text.
Seeing a blank page or "This site can't be reached"?
Make sure your terminal still shows "Server running on http://localhost:3000" without any errors. If the server stopped, run node server.js again.
If you see a blank page, confirm your index.html file is inside the public folder (not in the project root). Check the Explorer sidebar in VS Code to confirm the file tree shows public/index.html.
help me figure out why my Express server isn't showing my HTML page
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on.
ⓧ I'd like to double check the full code
Here is server.js in full:
import express from 'express';
const app = express();
const port = 3000;
app.use(express.json());
app.use(express.static('public'));
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
Here is public/index.html in full:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cardiology Translator</title>
</head>
<body>
<h1>Cardiology Translator</h1>
</body>
</html>
And your package.json should look like this (the exact version numbers after ^ may differ slightly):
{
"name": "clinic-translator",
"version": "1.0.0",
"type": "module",
"description": "Medical clinic translator powered by Claude",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.111.0",
"express": "^5.2.1"
}
}
Your project skeleton is ready. Next up, you'll build the bidirectional translator interface so the cardiologist and patient's family can communicate back and forth.
Build the Translator UI
Your server is running and serving a bare-bones page. Now you need to give the cardiology office something they can actually use for two-way communication.
In this step, you will build a bidirectional translator interface: a direction toggle (Doctor to Patient or Patient to Doctor), a text area, a language dropdown, a Translate button with text-to-speech playback, and an output panel. Both the cardiologist and the patient's family can hear translations spoken aloud.
In this step, get ready to:
- Build the HTML form with direction toggle, input, dropdown, button, and output elements.
- Style the page with a clean, professional cardiology look.
- Wire up the Translate button and a Listen button for text-to-speech playback.
Build the HTML structure
The translator needs a direction toggle so both sides can communicate, a text area for input, a dropdown to pick the target language, buttons for translating and listening, and an output area to display results.
- Open public/index.html in your editor and replace its entire content with the following page structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cardiology Translator</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>Cardiology Translator</h1>
<p class="subtitle">Bidirectional translation for cardiologists, patients, and families</p>
<div class="direction-toggle">
<button id="dir-doc" class="dir-btn active">Doctor to Patient</button>
<button id="dir-patient" class="dir-btn">Patient to Doctor</button>
</div>
<div class="translator">
<div class="input-section">
<label for="input-text" id="input-label">Cardiologist Instruction (English)</label>
<textarea id="input-text" placeholder="e.g., Your ejection fraction is 35%, we need to start you on a beta blocker" rows="5"></textarea>
</div>
What does this code do?
This sets up the page head with a link to style.css (which you will create next) and opens the main container. The two dir-btn buttons let users switch between Doctor-to-Patient and Patient-to-Doctor modes.
The input-section gives the user a labeled textarea to type or paste text. The placeholder shows a cardiology example so they know what to enter.
- Directly below that code, add the controls, output area, and closing tags:
<div class="controls">
<div class="language-select">
<label for="target-language">Translate to:</label>
<select id="target-language">
<option value="Spanish">Spanish</option>
<option value="Mandarin Chinese">Mandarin Chinese</option>
<option value="Arabic">Arabic</option>
<option value="Vietnamese">Vietnamese</option>
<option value="Korean">Korean</option>
<option value="French">French</option>
</select>
</div>
<button id="translate-btn">Translate</button>
<button id="listen-btn" disabled>Listen</button>
</div>
<div class="output-section">
<label for="output-text" id="output-label">Patient Translation</label>
<div id="output-text" class="output-box">Translation will appear here...</div>
</div>
</div>
<p class="disclaimer">This tool aids communication. Always confirm with a certified medical interpreter for critical information.</p>
</div>
<script src="app.js"></script>
</body>
</html>
What does this code do?
The select dropdown offers six languages commonly needed in cardiology clinics. The Listen button starts disabled and enables after a translation appears, letting users hear the result spoken aloud.
The disclaimer at the bottom reminds staff this is an aid, not a replacement for a certified interpreter. The script tag at the end loads app.js (which you will create shortly). The id attributes on the labels allow JavaScript to update them when the direction changes.
- Save public/index.html.
- Refresh your browser at http://localhost:3000. You should see a textarea, a language dropdown, and a Translate button. They will look unstyled for now.
Seeing a blank page?
Make sure you saved the file. Check that your server is still running in the terminal. If it stopped, restart it with node server.js.
If you see the old heading but not the form elements, confirm the entire file was replaced (not just appended to the old content).
Why isn't my HTML form showing up in the browser?
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm both code blocks are present in order. The first block starts with <!DOCTYPE html> and ends with </div> closing the input-section. The second block starts with the controls div and ends with </html>.
Style the page layout and container
The unstyled form works, but a medical tool needs to look professional and trustworthy. You will create a stylesheet that gives the page a clean, clinical appearance.
- Create a new file called style.css inside the public folder and add the base layout styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f0f4f8;
color: #1a202c;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
}
What does this code do?
The * reset removes default browser spacing so everything starts consistent. The body sets a light blue-grey background, and .container creates a centered white card with rounded corners and a subtle shadow.
- Below the container styles, add the typography and translator layout rules:
h1 {
color: #2c5282;
margin-bottom: 4px;
font-size: 1.8rem;
}
.subtitle {
color: #718096;
margin-bottom: 30px;
font-size: 1rem;
}
.translator {
display: flex;
flex-direction: column;
gap: 20px;
}
.input-section label,
.output-section label {
display: block;
font-weight: 600;
margin-bottom: 6px;
color: #4a5568;
font-size: 0.9rem;
}
What does this code do?
The heading gets a medical-blue color. The .translator uses flexbox with a column layout and 20px gaps between each section. Labels are bold and slightly smaller than body text to create a clear hierarchy.
- Save public/style.css and refresh your browser.
- You should see the page now has a white card centered on a light blue background. The heading is blue, and the labels above the textarea and output are styled.
Page still looks unstyled?
Check that your file is named exactly style.css (not styles.css) inside the public folder. The link tag in your HTML references style.css exactly.
Why aren't my CSS styles showing up?
Style the form inputs and controls
- Below your existing styles in style.css, add the textarea and controls layout:
textarea {
width: 100%;
padding: 14px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
resize: vertical;
transition: border-color 0.2s;
}
textarea:focus {
outline: none;
border-color: #4299e1;
}
.controls {
display: flex;
align-items: flex-end;
gap: 16px;
}
.language-select {
flex: 1;
}
What does this code do?
The textarea gets full width, comfortable padding, and a blue highlight on focus. The .controls row uses flexbox to place the dropdown and button side-by-side, with align-items: flex-end so they line up at the bottom.
- Continue adding the dropdown label, select element, and focus state:
.language-select label {
display: block;
font-weight: 600;
margin-bottom: 6px;
color: #4a5568;
font-size: 0.9rem;
}
select {
width: 100%;
padding: 10px 14px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
background: white;
cursor: pointer;
}
select:focus {
outline: none;
border-color: #4299e1;
}
- Save style.css and refresh your browser.
- The textarea should now have rounded borders with a blue highlight when you click into it. The dropdown sits neatly beside the Translate button area.
Style the button and output area
- Add the Translate button styles below your existing CSS:
#translate-btn {
padding: 10px 28px;
background: #2c5282;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
white-space: nowrap;
}
#translate-btn:hover {
background: #1a365d;
}
#translate-btn:disabled {
background: #a0aec0;
cursor: not-allowed;
}
What does this code do?
The button gets the same medical-blue as the heading. The :hover state darkens it for feedback, and :disabled greys it out when a translation is in progress (which you will wire up later).
- Finally, add the output box and disclaimer styles:
.output-box {
padding: 14px;
background: #f7fafc;
border: 2px solid #e2e8f0;
border-radius: 8px;
min-height: 80px;
font-size: 1.05rem;
color: #2d3748;
white-space: pre-wrap;
}
.output-box.loading {
color: #a0aec0;
font-style: italic;
}
.disclaimer {
margin-top: 24px;
padding: 12px;
background: #fffbeb;
border: 1px solid #f6e05e;
border-radius: 6px;
font-size: 0.8rem;
color: #744210;
text-align: center;
}
What does this code do?
The .output-box creates a lightly shaded read-only area where translations appear. The .loading class will grey out and italicize text while waiting for a response. The disclaimer uses a warm yellow background to draw attention without feeling alarming.
- Save style.css and refresh your browser.
- You should see a fully styled page: a dark blue Translate button, a light grey output panel, and a yellow disclaimer banner at the bottom.
Button or output area not styled?
Check that the button in your HTML has id="translate-btn" and the output div has class="output-box". CSS selectors are case-sensitive and must match your HTML exactly.
Why is my CSS not applying to specific elements?
✔️ Awesome, I've got everything!
Great. Make sure you have saved style.css before continuing.
ⓧ I'd like to double check the full code
Your style.css file should contain all six code blocks from above in order. Scroll up and confirm each is present: base layout (*, body, container), typography and translator layout, textarea and controls, language-select label and select, translate button, and output box with disclaimer.
Wire up the Translate button
The page looks right, but clicking the buttons does nothing yet. You need a JavaScript file that wires the direction toggle, confirms the Translate button works, and sets up text-to-speech playback via the browser's built-in Web Speech API.
- Create a new file called app.js inside the public folder and add the following code:
// Cache references to the DOM elements
const inputText = document.getElementById('input-text');
const targetLanguage = document.getElementById('target-language');
const translateBtn = document.getElementById('translate-btn');
const outputText = document.getElementById('output-text');
const listenBtn = document.getElementById('listen-btn');
const dirDocBtn = document.getElementById('dir-doc');
const dirPatientBtn = document.getElementById('dir-patient');
const inputLabel = document.getElementById('input-label');
const outputLabel = document.getElementById('output-label');
// Track the current direction
let direction = 'doc-to-patient';
// Direction toggle handlers
dirDocBtn.addEventListener('click', () => {
direction = 'doc-to-patient';
dirDocBtn.classList.add('active');
dirPatientBtn.classList.remove('active');
inputLabel.textContent = 'Cardiologist Instruction (English)';
outputLabel.textContent = 'Patient Translation';
});
dirPatientBtn.addEventListener('click', () => {
direction = 'patient-to-doc';
dirPatientBtn.classList.add('active');
dirDocBtn.classList.remove('active');
inputLabel.textContent = 'Patient/Family Message';
outputLabel.textContent = 'English for Cardiologist';
});
What does this code do?
The first block grabs references to all DOM elements. The direction variable tracks whether the user is translating doctor-to-patient or patient-to-doctor.
The direction toggle handlers update the labels and the active button styling. When the patient-to-doctor direction is active, the label changes to reflect that the patient or family is typing their message.
- Save public/app.js.
- Before you test, what do you expect to happen when you click the Translate button?
Refresh your browser and test both directions. Click Doctor to Patient and confirm the labels say "Cardiologist Instruction (English)" and "Patient Translation." Then click Patient to Doctor and confirm the labels switch to "Patient/Family Message" and "English for Cardiologist."
Nothing happens when you click Translate?
Open your browser's DevTools console (press Cmd+Option+J on macOS or Ctrl+Shift+J on Windows). Look for error messages. A common issue is a typo in an element ID that causes getElementById to return null.
Also confirm the <script src="app.js"></script> tag is at the bottom of your HTML, just before </body>. If it is in the <head>, the DOM elements do not exist yet when the script runs.
Why does my Translate button click do nothing?
✔️ Awesome, I've got everything!
Great. Double check you have saved all three files.
ⓧ I'd like to double check the full code
// Cache references to the DOM elements
const inputText = document.getElementById('input-text');
const targetLanguage = document.getElementById('target-language');
const translateBtn = document.getElementById('translate-btn');
const outputText = document.getElementById('output-text');
// Placeholder handler to confirm wiring works
translateBtn.addEventListener('click', () => {
outputText.textContent = 'Translation will appear here...';
});
Your translator has a professional bidirectional interface with text-to-speech support that a cardiology office could actually trust. Next up, you will connect this frontend to the Claude API so the Translate button produces real translations.
Connect to Claude for Translation
Your bidirectional UI is ready, but clicking Translate does nothing useful yet. In this step you will wire your Express backend to the Claude API so the button delivers real translations in both directions.
You will notice something interesting once it works: Claude translates the words accurately, but cardiology jargon like "ejection fraction" and "anticoagulation" stays intact. A patient reading the output still would not understand what to do.
In this step, get ready to:
- Create a /translate API endpoint that calls Claude.
- Connect the frontend Translate button to the backend.
- See your first real translation appear in the output area.
Create the /translate API endpoint
Your server needs to receive the medical text from the frontend, send it to Claude, and return the translated result. This means importing the Anthropic SDK, initializing a client, and adding a POST route.
- In server.js, find this line at the top of the file:
import express from 'express';
- Add the Anthropic import directly below it so the top of your file looks like this:
import express from 'express';
import Anthropic from '@anthropic-ai/sdk';
- Now find the two middleware lines. Add the Anthropic client initialization below them by pasting this code:
// Initialize the Anthropic client using your API key
const client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
What does this code do?
This creates an Anthropic client object that handles authentication and communication with the Claude API. It reads your ANTHROPIC_API_KEY environment variable so your secret key never appears in code.
- Find the app.listen block at the bottom of server.js. Add the /translate route above it by pasting this code:
app.post('/translate', async (req, res) => {
const { text, targetLanguage } = req.body;
if (!text || !targetLanguage) {
return res.status(400).json({ error: 'Missing text or targetLanguage' });
}
try {
// Call Claude with a simple translation prompt
const message = await client.messages.create({
model: 'claude-haiku-4-5',
max_tokens: 1024,
system: `You are a translator. Translate the following medical text to ${targetLanguage}.`,
messages: [{ role: 'user', content: text }],
});
const translatedText = message.content[0].text;
res.json({ translation: translatedText });
} catch (error) {
console.error('Translation error:', error.message);
res.status(500).json({ error: 'Translation failed. Please try again.' });
}
});
What does this code do?
This route accepts a POST request with text and targetLanguage in the body. It sends the medical text to Claude with a system prompt telling it to translate to the chosen language.
The model claude-haiku-4-5 is fast and cost-effective for translation tasks. Each translation costs roughly $0.001-0.003.
- Save server.js.
- Stop your running server by pressing Ctrl+C in the terminal.
- Restart the server with your API key by running:
ANTHROPIC_API_KEY=your-api-key-here node server.js
Already exported your API key?
If you exported ANTHROPIC_API_KEY earlier in your terminal session, you can simply run node server.js without the prefix. The prefix sets the variable for that single command only.
You should see Server running on http://localhost:3000 in the terminal.
- Test the endpoint directly by opening a new terminal tab and running:
curl -X POST http://localhost:3000/translate \
-H "Content-Type: application/json" \
-d '{"text": "Take two tablets daily", "targetLanguage": "Spanish"}'
You should see a JSON response containing a Spanish translation of the phrase.
Seeing an authentication error?
Make sure your ANTHROPIC_API_KEY is set correctly. Double-check that you copied the full key from your Anthropic dashboard with no extra spaces.
On Windows, use set ANTHROPIC_API_KEY=your-key-here in Command Prompt or $env:ANTHROPIC_API_KEY="your-key-here" in PowerShell before running node server.js.
Help me fix my Anthropic API key error
Connect the frontend to the backend
The endpoint works. Now you need the Translate button to call it instead of showing placeholder text.
- Open public/app.js and delete all the existing code. Replace it with the DOM references and translation function by pasting this code:
const inputText = document.getElementById('input-text');
const targetLanguage = document.getElementById('target-language');
const translateBtn = document.getElementById('translate-btn');
const outputText = document.getElementById('output-text');
// Send medical text to the backend for translation
async function translateText() {
const text = inputText.value.trim();
const language = targetLanguage.value;
if (!text) {
outputText.textContent = 'Please enter text to translate.';
return;
}
// Show loading state while waiting for Claude
translateBtn.disabled = true;
translateBtn.textContent = 'Translating...';
outputText.textContent = 'Translating...';
outputText.classList.add('loading');
What does this code do?
The function grabs the text from the input area and the selected language from the dropdown. If the text is empty, it shows a message and stops early.
The loading state disables the button and adds the loading CSS class so the user sees visual feedback while waiting for the API response.
- Directly below the code you just pasted (still inside the translateText function), add the fetch call and error handling:
try {
const response = await fetch('/translate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, targetLanguage: language }),
});
const data = await response.json();
if (response.ok) {
outputText.textContent = data.translation;
outputText.classList.remove('loading');
} else {
outputText.textContent = 'Error: ' + (data.error || 'Translation failed.');
outputText.classList.remove('loading');
}
} catch (error) {
outputText.textContent = 'Error: Could not connect to server.';
outputText.classList.remove('loading');
}
translateBtn.disabled = false;
translateBtn.textContent = 'Translate';
}
What does this code do?
The fetch call sends the medical text and language to your /translate endpoint. On success, it displays the translation. On failure, it shows an error message.
The translateBtn.disabled = false line re-enables the button after the request completes, regardless of success or failure.
- Finally, add the event listeners at the bottom of public/app.js to wire up the button and a keyboard shortcut:
// Wire the Translate button and Ctrl+Enter shortcut
translateBtn.addEventListener('click', translateText);
inputText.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'Enter') {
translateText();
}
});
What does this code do?
The first listener fires translateText when the user clicks the Translate button. The second lets users press Ctrl+Enter to trigger translation without reaching for the mouse.
- Save public/app.js.
Seeing a syntax error in the browser console?
Make sure you pasted the three blocks in order with no gap or extra closing brace between them. The try block must be inside the translateText function, and the event listeners must be outside it.
Help me fix my app.js syntax error
✔️ Awesome, I've got everything!
Great. Make sure you have saved both server.js and public/app.js.
ⓧ I'd like to double check the full code
Both files are longer than 30 lines at this point. Scroll up and confirm your code contains each of these blocks in order:
server.js:
- The two import lines at the top (express and @anthropic-ai/sdk).
- The app and port declarations plus the two app.use middleware lines.
- The client initialization block.
- The app.post('/translate', ...) route.
- The app.listen block at the very bottom.
public/app.js:
- Four const declarations grabbing DOM elements.
- The async function translateText() with validation, loading state, fetch call, and error handling.
- Two addEventListener calls at the bottom (click and keydown).
Test your first translation
Time to see the translator in action from the browser. Before you test, think about this: if you send a phrase full of medical jargon, do you expect Claude to explain the jargon in simple terms, or just translate the words literally?
- Open http://localhost:3000 in your browser (refresh if it was already open).
- In the text area, type:
The patient must remain NPO after midnight prior to the scheduled endoscopy
- Select Spanish from the language dropdown.
- Click Translate.
You see a Spanish translation appear in the output box. But look closely at the output.
The translation likely preserves medical terms literally. You will see abbreviations like "NPO" left unexplained, or "endoscopia" used without any clarification of what the procedure actually involves.
Why is this a problem?
A patient who does not speak English also likely does not know what "NPO" or "endoscopia" means. They need to hear something like "do not eat or drink" and "a procedure to look inside your stomach."
This is exactly where a basic translation API would stop. In the next step, you will redesign the prompt so Claude both translates AND simplifies in one pass.
Your translator is live and delivering real translations from Claude. Next up, you will engineer a smarter prompt that turns confusing medical jargon into plain language any patient can understand.
Engineer the Medical Simplification Prompt
In the last step, you got translation working. But you also noticed the problem: Claude translates words accurately while keeping confusing medical jargon intact.
A patient reading a literally translated term like "NPO" or "endoscopia" still has no idea what to actually do. This step fixes that gap by redesigning the system prompt to instruct Claude to both translate AND simplify to a 5th-grade reading level.
In this step, get ready to:
- Redesign the system prompt to simplify medical jargon while translating.
- Test the improved prompt with challenging medical phrases to verify the difference.
Redesign the system prompt
The current prompt gives Claude a single instruction: translate. It says nothing about simplifying terminology or adjusting reading level.
- In server.js, find the system parameter inside client.messages.create. It currently looks like this:
system: `You are a translator. Translate the following medical text to ${targetLanguage}.`,
- Replace that entire system line with this detailed medical translator prompt:
system: `You are a medical clinic translator. Your job is to translate medical instructions from English into ${targetLanguage} while simultaneously simplifying ALL medical terminology to a 5th-grade reading level.
Rules:
- Translate the text into ${targetLanguage}
- Replace ALL medical jargon, abbreviations, and technical terms with simple everyday words
- Explain medical abbreviations (e.g., NPO = do not eat or drink, BID = twice a day, TID = three times a day, PO = by mouth, PRN = as needed)
- Keep the output concise and actionable
- Use short sentences a patient with limited education can understand
- Do NOT include the original English text in your response
- Do NOT add any commentary or notes, just provide the translated and simplified text
Example:
Input: "Administer 500mg amoxicillin PO TID for 10 days"
Output in Spanish: "Tome una pastilla de amoxicilina (500mg) por la boca, 3 veces al dia, durante 10 dias."`,
What does this prompt do differently?
The old prompt had one job: translate. This new prompt has multiple jobs running simultaneously.
- It sets a 5th-grade reading level ceiling, forcing Claude to use simple words.
- It lists specific medical abbreviations (NPO, BID, TID, PO, PRN) with their plain-language equivalents, teaching Claude the pattern.
- The example input/output pair shows Claude exactly what good output looks like. This is called a "few-shot example" in prompt engineering.
- Save server.js.
- Switch to your terminal and stop the running server by pressing Ctrl+C.
- Restart the server by running:
ANTHROPIC_API_KEY=your-api-key-here node server.js
You should see Server running on http://localhost:3000 again in your terminal.
Server won't start?
If you see a syntax error, check that the template literal backticks are properly opened and closed around the system prompt. The prompt uses backticks (`) not regular quotes.
Make sure you replaced the entire old system line and did not accidentally leave part of the old string behind.
help me fix a syntax error in my server.js system prompt
Test with challenging medical phrases
Time to see if the new prompt actually makes a difference. You will test with the same phrase that produced jargon-filled output in the previous step.
- Return to your browser at http://localhost:3000.
- Type this phrase into the input area:
The patient must remain NPO after midnight prior to the scheduled endoscopy
- Select Spanish from the language dropdown.
Before you click Translate, think about what you expect to see this time. Will it still say "NPO" and "endoscopia" literally, or will it explain those terms in everyday words?
- Click Translate.
This time, the output explains NPO as "do not eat or drink" and endoscopy as something like "a procedure to look inside your stomach" in plain, patient-friendly Spanish. Compare that to the jargon-heavy output from the previous step.
Why is this better than Google Translate?
Google Translate would render "NPO" as "NPO" in any language. It cannot simplify what it does not understand contextually. Your Claude-powered translator understands medical context and rewrites the instruction so a patient with no medical background can follow it.
This is the core value of an LLM-powered translator for this use case: simultaneous translation plus simplification in a single pass.
- Now try a second challenging phrase. Type this into the input area:
Apply topical corticosteroid BID to affected area. Discontinue if erythema worsens.
- Click Translate with any language selected.
The output should replace "topical corticosteroid" with something like "skin cream", "BID" with "twice a day", and "erythema" with "redness". Every medical term becomes something a patient can act on.
Output still looks too technical?
If you still see medical jargon in the output, make sure you saved server.js AND restarted the server. The old prompt is cached in memory until you restart.
Check that you replaced the entire system value and that the new prompt starts with You are a medical clinic translator.
help me debug why my medical translator still shows jargon
✔️ Awesome, I've got everything!
Great. Double check you have saved server.js with the updated system prompt.
ⓧ I'd like to double check the full code
Your server.js should contain these sections in order:
- The import statements for express and Anthropic.
- The app setup with express.json() and express.static('public').
- The Anthropic client initialization.
- The POST /translate route with the detailed medical translator system prompt (starting with "You are a medical clinic translator").
- The app.listen call on port 3000.
Your translator now does what no basic translation API can: it translates and simplifies in a single pass. Next up, you will add a quick-phrase panel so clinic staff can translate common instructions with a single click.
Add Quick-Phrase Panel
Your translator now produces clear, simplified translations that patients can understand. In the previous step, you saw the difference thoughtful prompt engineering makes.
In a real clinic, staff type the same instructions over and over: fasting prep, medication reminders, follow-up scheduling. A quick-phrase panel gives them one-click access to pre-written instructions, saving time and ensuring consistent translations for the most common scenarios.
In this step, get ready to:
- Add a quick-phrase panel with five common clinic instructions.
- Wire each button to auto-fill the input and trigger translation.
Add a quick-phrase section to the HTML
Each button stores a full medical instruction in a data-phrase attribute. When clicked, that text fills the input area automatically.
- In public/index.html, find the closing </p> tag of the subtitle paragraph:
<p class="subtitle">Translate and simplify medical instructions for patients</p>
- Add the following quick-phrases section directly below that line, above the existing <div class="translator">:
<div class="quick-phrases">
<h3>Quick Phrases</h3>
<div class="phrase-buttons">
<button class="phrase-btn" data-phrase="Do not eat or drink anything after midnight the night before your procedure. This includes water, gum, and candy.">Fasting Instructions</button>
<button class="phrase-btn" data-phrase="Take your medication exactly as prescribed. Take one tablet by mouth twice daily with food. Do not skip doses.">Medication Reminder</button>
<button class="phrase-btn" data-phrase="You need to come back for a follow-up appointment in 2 weeks. Please call our office to schedule your next visit.">Follow-Up Appointment</button>
<button class="phrase-btn" data-phrase="Please bring your insurance card and a photo ID to your appointment. If you do not have insurance, bring proof of income.">Bring Insurance Card</button>
<button class="phrase-btn" data-phrase="Please arrive 15 minutes before your scheduled appointment time to complete paperwork and check in at the front desk.">Arrive 15 Minutes Early</button>
</div>
</div>
What does this code do?
- Each <button> has a class of phrase-btn so JavaScript can find them all at once.
- The data-phrase attribute stores the full medical instruction text. This keeps the button label short while storing the complete phrase to translate.
- The five phrases cover the most common clinic scenarios: fasting prep, medication guidance, follow-up care, insurance reminders, and arrival instructions.
- Save public/index.html.
- Refresh your browser at http://localhost:3000. You should see five unstyled buttons above the text area, each labeled with a phrase category like "Fasting Instructions" and "Medication Reminder".
Don't see the buttons?
- Make sure the new <div class="quick-phrases"> block sits between the subtitle </p> and the <div class="translator"> line.
- Check that you saved the file after pasting.
- help me find where to place the quick-phrases HTML
The buttons work structurally but look plain. Next, add styling to give them a polished, clickable appearance.
- In public/style.css, find the .subtitle rule:
.subtitle {
color: #718096;
margin-bottom: 30px;
font-size: 1rem;
}
- Add the following container and layout styles directly below the .subtitle rule:
.quick-phrases {
margin-bottom: 30px;
padding: 20px;
background: #ebf8ff;
border-radius: 8px;
}
.quick-phrases h3 {
color: #2c5282;
margin-bottom: 12px;
font-size: 0.95rem;
}
.phrase-buttons {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
What does this code do?
- The .quick-phrases container gets a soft blue background to visually separate it from the translator section below.
- The .phrase-buttons uses flexbox with flex-wrap: wrap so buttons flow onto a new row if the screen is narrow.
- Save public/style.css and refresh your browser.
- You should see a light blue panel with a "Quick Phrases" heading. The buttons inside still look like default browser buttons.
- Add the button styles directly below the .phrase-buttons rule you just added:
.phrase-btn {
padding: 8px 14px;
background: white;
border: 1px solid #bee3f8;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
color: #2c5282;
transition: background 0.2s;
}
.phrase-btn:hover {
background: #bee3f8;
}
What does this code do?
Each .phrase-btn gets a white background with a blue border, matching the medical theme. The hover effect darkens the background so staff can clearly see which button they are about to click.
- Save public/style.css and refresh your browser.
- The buttons now appear as neatly styled pill-shaped elements. Hovering over any button highlights it in a darker blue.
Buttons still look unstyled?
- Confirm the class on your buttons is exactly phrase-btn (with a hyphen). The CSS targets .phrase-btn so a mismatch means no styles apply.
- Check that the new CSS rules are placed after the .subtitle block and before the .translator block.
- help me debug my quick-phrase styling
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm the following are present in your files:
- In public/index.html: the <div class="quick-phrases"> section with five phrase-btn buttons, placed between the subtitle and the translator div.
- In public/style.css: five new rules (.quick-phrases, .quick-phrases h3, .phrase-buttons, .phrase-btn, .phrase-btn:hover) placed after the .subtitle rule.
Wire the quick-phrase buttons
The buttons look great but do nothing yet. You need JavaScript to grab the stored phrase from each button's data attribute, fill the text area, and fire off a translation automatically.
- In public/app.js, find this line near the top:
const outputText = document.getElementById('output-text');
- Add this line directly below it to select all phrase buttons:
const phraseButtons = document.querySelectorAll('.phrase-btn');
- Now find the line where the Translate button listener is attached:
translateBtn.addEventListener('click', translateText);
- Add the following block directly below that line to wire up each phrase button:
// Wire each quick-phrase button to auto-fill and translate
phraseButtons.forEach((btn) => {
btn.addEventListener('click', () => {
inputText.value = btn.dataset.phrase;
translateText();
});
});
What does this code do?
- The querySelectorAll('.phrase-btn') grabs every button with that class, returning a list you can loop over.
- The forEach attaches a click listener to each button. When clicked, it reads the full instruction from btn.dataset.phrase (the data-phrase attribute) and sets it as the text area value.
- Calling translateText() immediately after filling the input means one click does both: fill and translate. No extra step for the staff.
- Save public/app.js.
Clicking a button does nothing?
- Open your browser's DevTools console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows) and check for errors. A common one is a typo in .phrase-btn causing querySelectorAll to return an empty list.
- Confirm the phraseButtons const is declared at the top level of app.js, not inside a function.
- help me debug my quick-phrase button wiring
✔️ Awesome, I've got everything!
Great. Make sure app.js is saved before testing.
ⓧ I'd like to double check the full code
Scroll up and confirm the following are present in public/app.js:
- The const phraseButtons line directly after the outputText declaration.
- The phraseButtons.forEach block placed between translateBtn.addEventListener('click', translateText) and the inputText.addEventListener('keydown'...) block.
Time to test the full workflow. Before you click, think: what do you expect to see in the output area when you click a quick-phrase button?
- Refresh your browser at http://localhost:3000.
- Make sure Spanish is selected in the language dropdown.
- Click the Fasting Instructions button.
Within a couple of seconds, the output area shows a patient-friendly Spanish translation. The input text area auto-fills with the fasting instruction, and the translation simplifies it into plain language a patient can follow.
Why is this better than typing every time?
In a busy clinic, a receptionist might translate fasting instructions 20 times a day. One click replaces 20 rounds of typing, copying, and pasting. The stored phrases are also pre-tested, so staff know the translations will be clear and consistent.
Your medical translator is now a practical clinic tool. Staff can translate custom instructions or use quick phrases for the most common scenarios. You have built a full-stack app that combines API integration, prompt engineering, and a thoughtful UI to solve a real communication problem.
Secret mission
Add Text-to-Speech for Patients
Many patients in a clinic setting have limited literacy in their own language. What if the app could speak the translation aloud so the patient can hear it directly? Add browser-based text-to-speech to make this tool useful in a real waiting room.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing cloud costs. You only pay for Claude API calls when you actively use the translator.
Resources you used:
- The clinic-translator project folder on your Desktop (contains server.js, public/ folder, and node_modules).
- The running Express server process at http://localhost:3000.
- Your Anthropic API key (stored as an environment variable in your terminal session).
Keep everything running
No action needed. Choose this if you are still actively building or want to continue experimenting with the translator.
- Your server continues running at http://localhost:3000 until you close the terminal or press Ctrl+C.
- Your project files remain in the clinic-translator folder on your Desktop.
- Your Anthropic API key stays set in your current terminal session. It will be cleared when you close the terminal.
Pause - I'll come back to this later
Stop the running server to free up your terminal. Your project files stay on disk so you can restart anytime.
- Switch to the terminal where your server is running and press Ctrl+C to stop it.
- To restart later, open a terminal in your clinic-translator folder, set your API key again, and run node server.js.
Delete - I don't want to use this again
Remove all project files and start fresh. This is permanent.
- Switch to the terminal where your server is running and press Ctrl+C to stop it.
- Delete the project folder by running:
macOS
cd ~/Desktop
rm -rf clinic-translator
Windows
cd ~\Desktop
rmdir /s /q clinic-translator
- Unset the Anthropic API key from your current terminal session:
macOS
unset ANTHROPIC_API_KEY
Windows
Remove-Item Env:ANTHROPIC_API_KEY
- Confirm the folder is gone by running ls ~/Desktop (macOS) or dir ~\Desktop (Windows). You should no longer see clinic-translator listed.
Nice Work!
Nice Work!
You just built a medical clinic translator that does something no basic translation API can do on its own. Your app simultaneously translates AND simplifies medical jargon into plain language that patients can actually understand.
You've learned how to:
- Build a full-stack web application with a Node.js/Express backend that securely calls the Claude API and serves a responsive HTML/CSS/JavaScript frontend.
- Integrate the Anthropic SDK to perform intelligent, context-aware translations using claude-haiku-4-5 as the model.
- Practice prompt engineering by iterating from a naive translation prompt to a medical-specific system prompt that simplifies jargon to a 5th-grade reading level for patients.
- Secret Mission: Added text-to-speech using the Web Speech API so patients can hear translations spoken aloud in their language.
Ready to quiz yourself?