Today, I want to present this tool I wrote in NodeJS - it accepts a single Chinese name, in Chinese characters, and provides transliterations of the name in various languages.
Setup
After installing my project using the node command, I installed Express and Handlebars.npm install --save express
npm install --save express-handlebars
npm install --save express-handlebars
This is what exists in app.js, as a baseline. We load Express and Handlebars, and ensure that the view engine is Handlebars. We also ensure that main is the default layout, and we'll create that soon. We also declare assets as the folder where static content is served, and set the default page to serve the view for home. And then set the 404 and 500 pages. Lastly, we start the app up using the listen() method.
app.js
var express = require("express");
var app = express();
var handlebars = require("express-handlebars").create({defaultLayout:"main"});
app.engine("handlebars", handlebars.engine);
app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);
app.use(express.static("assets"));
app.get("/", (req, res)=> {
res.render("home");
});
app.use((req, res, next)=> {
res.status(404);
res.render("404");
});
app.use((err, req, res, next)=> {
res.status(500);
res.render("500", { errorMessage: err.code });
});
app.listen(app.get("port"), ()=> {
});
var app = express();
var handlebars = require("express-handlebars").create({defaultLayout:"main"});
app.engine("handlebars", handlebars.engine);
app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);
app.use(express.static("assets"));
app.get("/", (req, res)=> {
res.render("home");
});
app.use((req, res, next)=> {
res.status(404);
res.render("404");
});
app.use((err, req, res, next)=> {
res.status(500);
res.render("500", { errorMessage: err.code });
});
app.listen(app.get("port"), ()=> {
});
This is the main layout file. Note the links to CSS and JavaScript. In the body, we have a div with id pnlContainer, which will contain the content of whatever page the app serves.
views/layouts/main.handlebars
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chinese Name Transliterator</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<script type="text/javascript" src="js/functions.js"></script>
</head>
<body>
<div id="pnlContainer">
{{{ body }}}
</div>
</body>
</html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chinese Name Transliterator</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<script type="text/javascript" src="js/functions.js"></script>
</head>
<body>
<div id="pnlContainer">
{{{ body }}}
</div>
</body>
</html>
Let's just create a very basic CSS file that only specifies font. We can add more later.
asset/css/styles.css
body
{
font-size: 16px;
font-family: georgia;
}
{
font-size: 16px;
font-family: georgia;
}
In the JavaScript, we'll just have one single function, to check if a string is made up of only Chinese characters. To do that, we use a Regular Expression of a range of Chinese characters, then use the test() method with name as the argument, returning the result. That's also incidentally the only JavaScript validation we'll need in here.
asset/js/functions.js
function isValidChineseName(name) {
const re = /^[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF]+$/;
return re.test(name);
}
const re = /^[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF]+$/;
return re.test(name);
}
Here's the view for 404s.
views/404.handlebars
<h1>404</h1>
<p>Not found!</p>
<p>Not found!</p>
And for error 500s.
views/500.handlebars
<h1>500</h1>
<p>There was an error.</p>
<p><b>{{ errorMessage }}</b></p>
<p>There was an error.</p>
<p><b>{{ errorMessage }}</b></p>
And here's the default page. You'll notice there is no form tag; we won't need one because I don't actually intend to do any form submission here. What we do have is a nice header, a div (id pnlSelection), a button (id btnTransliterate) and a span tag (id pnlError) for showing errors. And a script tag for more shenanigans later.
views/home.handlebars
<h1>Chinese Name Transliterator</h1>
<div id="pnlSelection">
</div>
<button id="btnTransliterate">TRANSLITERATE</button>
<span id="pnlError"></span>
<script>
</script>
<div id="pnlSelection">
</div>
<button id="btnTransliterate">TRANSLITERATE</button>
<span id="pnlError"></span>
<script>
</script>
In pnlSelection, we have two more divs. One contains a label, and the other contains a textbox. The id of the textbox is txtFullChineseName and it will have at most 4 characters, unless you feel like transliterating full Manchurian names or something.
views/home.handlebars
<h1>Chinese Name Transliterator</h1>
<div id="pnlSelection">
<div><label for="txtFullChineseName">Full Chinese Name</label></div>
<div><input id="txtFullChineseName" maxlength="4" /></div>
</div>
<button id="btnTransliterate">TRANSLITERATE</button>
<span id="pnlError"></span>
<script>
</script>
<div id="pnlSelection">
<div><label for="txtFullChineseName">Full Chinese Name</label></div>
<div><input id="txtFullChineseName" maxlength="4" /></div>
</div>
<button id="btnTransliterate">TRANSLITERATE</button>
<span id="pnlError"></span>
<script>
</script>
This probably looks a mess. Time for more CSS.
pnlContainer fits the full height of the window (thus 100vh), has a maximum width of 400 pixels, which will fit nicely on mobile, and is centered in the middle of the window via the margin property. Text is centered by default.
asset/css/styles.css
body
{
font-size: 16px;
font-family: georgia;
}
#pnlContainer
{
min-height: 100vh;
max-width: 400px;
margin: 0 auto 0 auto;
text-align: center;
}
{
font-size: 16px;
font-family: georgia;
}
#pnlContainer
{
min-height: 100vh;
max-width: 400px;
margin: 0 auto 0 auto;
text-align: center;
}
pnlSelection is slightly more interesting. The display property is grid, which instantly turns it into a grid container. grid-template-columns has a value of "1fr 1fr" which basically means two by two, and I've specified a 10-pixel gap. The text-align property has been set to justify to offset the text centering of its parent.
asset/css/styles.css
body
{
font-size: 16px;
font-family: georgia;
}
#pnlContainer
{
min-height: 100vh;
max-width: 400px;
margin: 0 auto 0 auto;
text-align: center;
}
#pnlSelection
{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
text-align: justify;
}
{
font-size: 16px;
font-family: georgia;
}
#pnlContainer
{
min-height: 100vh;
max-width: 400px;
margin: 0 auto 0 auto;
text-align: center;
}
#pnlSelection
{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
text-align: justify;
}
Now for pnlError and the button. pnlError has red text. The button is pill-shaped and has large text. Nothing to see here, move along.
asset/css/styles.css
body
{
font-size: 16px;
font-family: georgia;
}
#pnlContainer
{
min-height: 100vh;
max-width: 400px;
margin: 0 auto 0 auto;
text-align: center;
}
#pnlSelection
{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
text-align: justify;
}
#pnlError
{
font-size: 0.8em;
font-weight: bold;
color: #FF0000;
}
button
{
width: 100%;
height: 2em;
margin-top: 2em;
border-radius: 20px;
}
{
font-size: 16px;
font-family: georgia;
}
#pnlContainer
{
min-height: 100vh;
max-width: 400px;
margin: 0 auto 0 auto;
text-align: center;
}
#pnlSelection
{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
text-align: justify;
}
#pnlError
{
font-size: 0.8em;
font-weight: bold;
color: #FF0000;
}
button
{
width: 100%;
height: 2em;
margin-top: 2em;
border-radius: 20px;
}
Time for a preview! So far so good.
We're not just going to have a Chinese name transliterated - we're also going to select the languages it is transliterated in. For that, let's created options.js in the data folder of the assets folder. We basically export an array of objects. Each object has two properties - name and label. These are the five romanization systems to be transliterated in - Wade-Giles, Hokkien, Jyutping, Hangul and Vietnamese.
assets/data/options.js
const options = [
{
value: "wade-giles",
label: "Wade-Giles",
},
{
value: "hokkien",
label: "Hokkien",
},
{
value: "jyutping",
label: "Cantonese",
},
{
value: "hangul",
label: "Korean",
},
{
value: "vietnamese",
label: "Vietnamese",
}
];
module.exports = options;
{
value: "wade-giles",
label: "Wade-Giles",
},
{
value: "hokkien",
label: "Hokkien",
},
{
value: "jyutping",
label: "Cantonese",
},
{
value: "hangul",
label: "Korean",
},
{
value: "vietnamese",
label: "Vietnamese",
}
];
module.exports = options;
We also have to make sure it's available in the back-end. Declare options as what options.js exports, then pass it into home as options.
app.js
const options = require("./assets/data/options.js");
var express = require("express");
var app = express();
var handlebars = require("express-handlebars").create({defaultLayout:"main"});
app.engine("handlebars", handlebars.engine);
app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);
app.use(express.static("assets"));
app.get("/", (req, res)=> {
res.render("home", { options: options });
});
app.use((req, res, next)=> {
res.status(404);
res.render("404");
});
app.use((err, req, res, next)=> {
res.status(500);
res.render("500", { errorMessage: err.code });
});
app.listen(app.get("port"), ()=> {
});
var express = require("express");
var app = express();
var handlebars = require("express-handlebars").create({defaultLayout:"main"});
app.engine("handlebars", handlebars.engine);
app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);
app.use(express.static("assets"));
app.get("/", (req, res)=> {
res.render("home", { options: options });
});
app.use((req, res, next)=> {
res.status(404);
res.render("404");
});
app.use((err, req, res, next)=> {
res.status(500);
res.render("500", { errorMessage: err.code });
});
app.listen(app.get("port"), ()=> {
});
Now in the view, we want to iterate through options, which has been passed down to this view from app.js. For each option, we'll have a div and a span. The first div will contain a checkbox and label, with values reflecting those of the option. The span tag will have an id that is "result_" concatenated with the name property of the current element of options.
views/home.handlebars
<div id="pnlSelection">
<div><label for="txtFullChineseName">Full Chinese Name</label></div>
<div><input id="txtFullChineseName" maxlength="4" /></div>
{{#each options}}
<div>
<label>
<input type="checkbox" name="languages" value="{{value}}">
{{label}}
</label>
</div>
<div>
<span class="txtResult" id="result_{{value}}"></span>
</div>
{{/each}}
</div>
<div><label for="txtFullChineseName">Full Chinese Name</label></div>
<div><input id="txtFullChineseName" maxlength="4" /></div>
{{#each options}}
<div>
<label>
<input type="checkbox" name="languages" value="{{value}}">
{{label}}
</label>
</div>
<div>
<span class="txtResult" id="result_{{value}}"></span>
</div>
{{/each}}
</div>
That's how it looks like! We haven't styled the span tags yet, and they have no content at the moment, so they're invisible.
Now, we want to handle clicking! Here's some beginning code...
views/home.handlebars
<script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
});
</script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
});
</script>
And then we want to start with validation. First, we clear the contents of pnlError. If the string given is not a valid Chinese name (remember the function we created earlier?), we set the contents of pnlError to a message, then exit early.
<script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
document.getElementById("pnlError").textContent = "";
const name = document.getElementById("txtFullChineseName").value.trim();
if (!isValidChineseName(name)) {
document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
return;
}
});
</script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
document.getElementById("pnlError").textContent = "";
const name = document.getElementById("txtFullChineseName").value.trim();
if (!isValidChineseName(name)) {
document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
return;
}
});
</script>
If no options are selected, we also set the error message and exit early. selectedOptions can be reused later.
views/home.handlebars
<script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
document.getElementById("pnlError").textContent = "";
const name = document.getElementById("txtFullChineseName").value.trim();
if (!isValidChineseName(name)) {
document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
return;
}
const selectedOptions = document.querySelectorAll("input[name='languages']:checked");
if (selectedOptions.length === 0) {
document.getElementById("pnlError").textContent = "Please select at least one transliteration option.";
return;
}
});
</script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
document.getElementById("pnlError").textContent = "";
const name = document.getElementById("txtFullChineseName").value.trim();
if (!isValidChineseName(name)) {
document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
return;
}
const selectedOptions = document.querySelectorAll("input[name='languages']:checked");
if (selectedOptions.length === 0) {
document.getElementById("pnlError").textContent = "Please select at least one transliteration option.";
return;
}
});
</script>
See what happens when the name contains anything that isn't Chinese.
Or when no option are checked. I'll be using this name as an example for the rest of this tutorial.
Then we declare properties, which really is a comma-separated string of all values selected. For this, we iterate through selectedOptions using the map() method, grabbing the value property values and appending them using the join() method.
views/home.handlebars
<script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
document.getElementById("pnlError").textContent = "";
const name = document.getElementById("txtFullChineseName").value.trim();
if (!isValidChineseName(name)) {
document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
return;
}
const selectedOptions = document.querySelectorAll("input[name='languages']:checked");
if (selectedOptions.length === 0) {
document.getElementById("pnlError").textContent = "Please select at least one transliteration option.";
return;
}
let properties = [...selectedOptions]
.map(x => `"${x.value}"`)
.join(", ");
});
</script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
document.getElementById("pnlError").textContent = "";
const name = document.getElementById("txtFullChineseName").value.trim();
if (!isValidChineseName(name)) {
document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
return;
}
const selectedOptions = document.querySelectorAll("input[name='languages']:checked");
if (selectedOptions.length === 0) {
document.getElementById("pnlError").textContent = "Please select at least one transliteration option.";
return;
}
let properties = [...selectedOptions]
.map(x => `"${x.value}"`)
.join(", ");
});
</script>
We'll create a prompt, prompt. For this, we want a valid JSON object that will contain named properties that the user has selected. So if the user checked on the checkboxes labelled "Cantonese" and "Korean", the resultant value inserted into prompt would be "jyutping,hangul".
views/home.handlebars
<script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
document.getElementById("pnlError").textContent = "";
const name = document.getElementById("txtFullChineseName").value.trim();
if (!isValidChineseName(name)) {
document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
return;
}
const selectedOptions = document.querySelectorAll("input[name='languages']:checked");
if (selectedOptions.length === 0) {
document.getElementById("pnlError").textContent = "Please select at least one transliteration option.";
return;
}
let properties = [...selectedOptions]
.map(x => `"${x.value}"`)
.join(", ");
let prompt = `Return only a valid JSON object. The object should contain the following properties only: ${properties}. The value of each property should be the equivalent romanized version of the Chinese name "${name}" (without intonations) only. In the event of ambiguity, pick the first reasonable result.`;
});
</script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
document.getElementById("pnlError").textContent = "";
const name = document.getElementById("txtFullChineseName").value.trim();
if (!isValidChineseName(name)) {
document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
return;
}
const selectedOptions = document.querySelectorAll("input[name='languages']:checked");
if (selectedOptions.length === 0) {
document.getElementById("pnlError").textContent = "Please select at least one transliteration option.";
return;
}
let properties = [...selectedOptions]
.map(x => `"${x.value}"`)
.join(", ");
let prompt = `Return only a valid JSON object. The object should contain the following properties only: ${properties}. The value of each property should be the equivalent romanized version of the Chinese name "${name}" (without intonations) only. In the event of ambiguity, pick the first reasonable result.`;
});
</script>
Then we use a Try-catch block to send the prompt to the transliterate route using the asynchronous fetch() function. We'll display an error if there's one.
views/home.handlebars
<script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
document.getElementById("pnlError").textContent = "";
const name = document.getElementById("txtFullChineseName").value.trim();
if (!isValidChineseName(name)) {
document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
return;
}
const selectedOptions = document.querySelectorAll("input[name='languages']:checked");
if (selectedOptions.length === 0) {
document.getElementById("pnlError").textContent = "Please select at least one transliteration option.";
return;
}
let properties = [...selectedOptions]
.map(x => `"${x.value}"`)
.join(", ");
let prompt = `Return only a valid JSON object. The object should contain the following properties only: ${properties}. The value of each property should be the equivalent English romanized version of the Chinese name "${name}" (without intonations) only. In the event of ambiguity, pick the first reasonable result.`;
try {
const response = await fetch("/transliterate", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt: prompt
})
});
} catch (err) {
document.getElementById("pnlError").textContent = "An unexpected error occurred.";
console.error(err);
}
});
</script>
document.getElementById("btnTransliterate").addEventListener("click", async () => {
document.getElementById("pnlError").textContent = "";
const name = document.getElementById("txtFullChineseName").value.trim();
if (!isValidChineseName(name)) {
document.getElementById("pnlError").textContent = "Please enter a Chinese name using Chinese characters only.";
return;
}
const selectedOptions = document.querySelectorAll("input[name='languages']:checked");
if (selectedOptions.length === 0) {
document.getElementById("pnlError").textContent = "Please select at least one transliteration option.";
return;
}
let properties = [...selectedOptions]
.map(x => `"${x.value}"`)
.join(", ");
let prompt = `Return only a valid JSON object. The object should contain the following properties only: ${properties}. The value of each property should be the equivalent English romanized version of the Chinese name "${name}" (without intonations) only. In the event of ambiguity, pick the first reasonable result.`;
try {
const response = await fetch("/transliterate", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt: prompt
})
});
} catch (err) {
document.getElementById("pnlError").textContent = "An unexpected error occurred.";
console.error(err);
}
});
</script>
Now let's handle the transliterate route. First, we set up the api.js file. org and key are derived from the OpenAI API project you should have set up for this project. Obviously my org and key values aren't "xx", but you'll have to get your own.
api.js
module.exports = {
org: "xx",
key: "xx"
}
org: "xx",
key: "xx"
}
Now, in app.js, define api as the exported value from api.js. Ensure that app uses the json() method from express. That's what we'll use to parse. And then define the transliterate route. It's POST, and async.
app.js
const api = require("./api.js");
const options = require("./assets/data/options.js");
var express = require("express");
var app = express();
var handlebars = require("express-handlebars").create({defaultLayout:"main"});
app.engine("handlebars", handlebars.engine);
app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);
app.use(express.static("assets"));
app.get("/", (req, res)=> {
res.render("home", { options: options });
});
app.use(express.json());
app.post("/transliterate", async (req, res) => {
});
app.use((req, res, next)=> {
res.status(404);
res.render("404");
});
app.use((err, req, res, next)=> {
res.status(500);
res.render("500", { errorMessage: err.code });
});
app.listen(app.get("port"), ()=> {
});
const options = require("./assets/data/options.js");
var express = require("express");
var app = express();
var handlebars = require("express-handlebars").create({defaultLayout:"main"});
app.engine("handlebars", handlebars.engine);
app.set("view engine", "handlebars");
app.set("port", process.env.PORT || 3000);
app.use(express.static("assets"));
app.get("/", (req, res)=> {
res.render("home", { options: options });
});
app.use(express.json());
app.post("/transliterate", async (req, res) => {
});
app.use((req, res, next)=> {
res.status(404);
res.render("404");
});
app.use((err, req, res, next)=> {
res.status(500);
res.render("500", { errorMessage: err.code });
});
app.listen(app.get("port"), ()=> {
});
We have a Try-catch block here.
app.post("/transliterate", async (req, res) => {
try {
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
try {
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
We obtain prompt by getting it from the body object of the request, req. If prompt does not exist, we exit early with an error.
app.js
app.post("/transliterate", async (req, res) => {
try {
const prompt = req.body.prompt;
if (!prompt) {
return res.status(400).json({
success: false,
error: "Prompt is required."
});
}
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
try {
const prompt = req.body.prompt;
if (!prompt) {
return res.status(400).json({
success: false,
error: "Prompt is required."
});
}
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
So here, we create response. It is what's returned from running fetch() with OpenAI's API responses endpoint, The operation is a POST and the headers passed in are Authorization, which uses the key value from api; and Content-Type which is set to accept JSON.
app.js
app.post("/transliterate", async (req, res) => {
try {
const prompt = req.body.prompt;
if (!prompt) {
return res.status(400).json({
success: false,
error: "Prompt is required."
});
}
const response = await fetch(
"https://api.openai.com/v1/responses",
{
method: "POST",
headers: {
"Authorization": `Bearer ${api.key}`,
"Content-Type": "application/json"
}
}
);
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
try {
const prompt = req.body.prompt;
if (!prompt) {
return res.status(400).json({
success: false,
error: "Prompt is required."
});
}
const response = await fetch(
"https://api.openai.com/v1/responses",
{
method: "POST",
headers: {
"Authorization": `Bearer ${api.key}`,
"Content-Type": "application/json"
}
}
);
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
Then in the body, we send a JSON-encoded string of an object containing the model and input. The model is "gpt-5" and the value for input is set to the string known as prompt. In effect, we're asking the API endpoint to provided romanizations for the provided Chinese name, in the various selected languages.
app.js
app.post("/transliterate", async (req, res) => {
try {
const prompt = req.body.prompt;
if (!prompt) {
return res.status(400).json({
success: false,
error: "Prompt is required."
});
}
const response = await fetch(
"https://api.openai.com/v1/responses",
{
method: "POST",
headers: {
"Authorization": `Bearer ${api.key}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "gpt-5",
input: prompt
})
}
);
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
try {
const prompt = req.body.prompt;
if (!prompt) {
return res.status(400).json({
success: false,
error: "Prompt is required."
});
}
const response = await fetch(
"https://api.openai.com/v1/responses",
{
method: "POST",
headers: {
"Authorization": `Bearer ${api.key}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "gpt-5",
input: prompt
})
}
);
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
Since we used await for the previous line, the next part will have response ready. If the ok property of response is false or does not exist, exit with an error.
app.js
app.post("/transliterate", async (req, res) => {
try {
const prompt = req.body.prompt;
if (!prompt) {
return res.status(400).json({
success: false,
error: "Prompt is required."
});
}
const response = await fetch(
"https://api.openai.com/v1/responses",
{
method: "POST",
headers: {
"Authorization": `Bearer ${api.key}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "gpt-5",
input: prompt
})
}
);
if (!response.ok) {
const errorText = await response.text();
return res.status(response.status).json({
success: false,
error: errorText
});
}
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
try {
const prompt = req.body.prompt;
if (!prompt) {
return res.status(400).json({
success: false,
error: "Prompt is required."
});
}
const response = await fetch(
"https://api.openai.com/v1/responses",
{
method: "POST",
headers: {
"Authorization": `Bearer ${api.key}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "gpt-5",
input: prompt
})
}
);
if (!response.ok) {
const errorText = await response.text();
return res.status(response.status).json({
success: false,
error: errorText
});
}
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
At this point, if there has been no early exit, that means the call was successful. We define data by converting it to JSON using the async method json(). Then we return the data. It looks convoluted right now because that's the way OpenAI wraps it - the text property of the first object of the content array, which in turn is the second element of the output array of data.
app.js
app.post("/transliterate", async (req, res) => {
try {
const prompt = req.body.prompt;
if (!prompt) {
return res.status(400).json({
success: false,
error: "Prompt is required."
});
}
const response = await fetch(
"https://api.openai.com/v1/responses",
{
method: "POST",
headers: {
"Authorization": `Bearer ${api.key}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "gpt-5",
input: prompt
})
}
);
if (!response.ok) {
const errorText = await response.text();
return res.status(response.status).json({
success: false,
error: errorText
});
}
const data = await response.json();
res.json({
success: true,
result: data.output[1].content[0].text
});
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
try {
const prompt = req.body.prompt;
if (!prompt) {
return res.status(400).json({
success: false,
error: "Prompt is required."
});
}
const response = await fetch(
"https://api.openai.com/v1/responses",
{
method: "POST",
headers: {
"Authorization": `Bearer ${api.key}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "gpt-5",
input: prompt
})
}
);
if (!response.ok) {
const errorText = await response.text();
return res.status(response.status).json({
success: false,
error: errorText
});
}
const data = await response.json();
res.json({
success: true,
result: data.output[1].content[0].text
});
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
Back to the view! We define data as the JSON shape of response. If success was not defined, then we set the error message in pnlError and exit early.
home.handlebars
try {
const response = await fetch("/transliterate", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt: prompt
})
});
const data = await response.json();
if (!data.success) {
document.getElementById("pnlError").textContent = data.error;
return;
}
} catch (err) {
document.getElementById("pnlError").textContent = "An unexpected error occurred.";
console.error(err);
}
const response = await fetch("/transliterate", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt: prompt
})
});
const data = await response.json();
if (!data.success) {
document.getElementById("pnlError").textContent = data.error;
return;
}
} catch (err) {
document.getElementById("pnlError").textContent = "An unexpected error occurred.";
console.error(err);
}
Define transliterations. It'll be the object data's result object. Then we have txtResult, declared as a collection of all elements in the DOM using the CSS class txtResult.
home.handlebars
try {
const response = await fetch("/transliterate", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt: prompt
})
});
const data = await response.json();
if (!data.success) {
document.getElementById("pnlError").textContent = data.error;
return;
}
const transliterations = JSON.parse(data.result);
const txtResult = document.getElementsByClassName("txtResult");
} catch (err) {
document.getElementById("pnlError").textContent = "An unexpected error occurred.";
console.error(err);
}
const response = await fetch("/transliterate", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt: prompt
})
});
const data = await response.json();
if (!data.success) {
document.getElementById("pnlError").textContent = data.error;
return;
}
const transliterations = JSON.parse(data.result);
const txtResult = document.getElementsByClassName("txtResult");
} catch (err) {
document.getElementById("pnlError").textContent = "An unexpected error occurred.";
console.error(err);
}
We convert it to an array, then run a forEach() to iterate through it. x is the current element. We get the id and strip "result_" from it to get the language that it's supposed to be in. Then from there, we populate x with its relevant transliteration, if it exists.
home.handlebars
try {
const response = await fetch("/transliterate", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt: prompt
})
});
const data = await response.json();
if (!data.success) {
document.getElementById("pnlError").textContent = data.error;
return;
}
const transliterations = JSON.parse(data.result);
const txtResult = document.getElementsByClassName("txtResult");
Array.from(txtResult).forEach((x) => {
let propName = x.id.replace("result_", "");
x.textContent = (transliterations[propName] ? transliterations[propName] : "");
});
} catch (err) {
document.getElementById("pnlError").textContent = "An unexpected error occurred.";
console.error(err);
}
const response = await fetch("/transliterate", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt: prompt
})
});
const data = await response.json();
if (!data.success) {
document.getElementById("pnlError").textContent = data.error;
return;
}
const transliterations = JSON.parse(data.result);
const txtResult = document.getElementsByClassName("txtResult");
Array.from(txtResult).forEach((x) => {
let propName = x.id.replace("result_", "");
x.textContent = (transliterations[propName] ? transliterations[propName] : "");
});
} catch (err) {
document.getElementById("pnlError").textContent = "An unexpected error occurred.";
console.error(err);
}
Try this! Enter a name. Then select a couple of languages. Let's try Wade-Giles, Cantonese and Korean.
It works! It takes a bit of time, but it works. In Wade-Giles, the name is "Tu Yu-lei", and so on.
Here's an improvement...
Add this to the layout file. It's another div, id pnlOverlay. In it, you have a message and an hourglass icon.views/layouts/main.handlebars
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chinese Name Transliterator</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<script type="text/javascript" src="js/functions.js"></script>
</head>
<body>
<div id="pnlOverlay">
<p><h1>Please wait.</h1>This may take<br />a minute...</p>
<p class="hourglass">⧗</p>
</div>
<div id="pnlContainer">
{{{ body }}}
</div>
</body>
</html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chinese Name Transliterator</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<script type="text/javascript" src="js/functions.js"></script>
</head>
<body>
<div id="pnlOverlay">
<p><h1>Please wait.</h1>This may take<br />a minute...</p>
<p class="hourglass">⧗</p>
</div>
<div id="pnlContainer">
{{{ body }}}
</div>
</body>
</html>
Style it this way. The position property is fixed, and we anchor it to the top left corner of the screen, with full height and width, a translucent black background and white text. Classic overlay. Of course, we have to set display to none, to hide it. The hourglass CSS class just makes sure the font for this is huge.
assets/css/styles.css
#pnlOverlay
{
position: fixed;
display: none;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
color: rgb(255, 255, 255);
text-align: center;
}
#pnlContainer
{
min-height: 100vh;
max-width: 400px;
margin: 0 auto 0 auto;
text-align: center;
}
#pnlSelection
{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
text-align: justify;
}
#pnlError
{
font-size: 0.8em;
font-weight: bold;
color: #FF0000;
}
.txtResult
{
font-weight: bold;
color: rgb(100, 100,100);
}
.hourglass
{
font-size: 5em;
}
button
{
width: 100%;
height: 2em;
margin-top: 2em;
border-radius: 20px;
}
{
position: fixed;
display: none;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
color: rgb(255, 255, 255);
text-align: center;
}
#pnlContainer
{
min-height: 100vh;
max-width: 400px;
margin: 0 auto 0 auto;
text-align: center;
}
#pnlSelection
{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
text-align: justify;
}
#pnlError
{
font-size: 0.8em;
font-weight: bold;
color: #FF0000;
}
.txtResult
{
font-weight: bold;
color: rgb(100, 100,100);
}
.hourglass
{
font-size: 5em;
}
button
{
width: 100%;
height: 2em;
margin-top: 2em;
border-radius: 20px;
}
For the heck of it, throw in an animation!
assets/css/styles.css
#pnlOverlay
{
position: fixed;
display: none;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
color: rgb(255, 255, 255);
text-align: center;
animation: pulse 1s infinite;
}
#pnlContainer
{
min-height: 100vh;
max-width: 400px;
margin: 0 auto 0 auto;
text-align: center;
}
#pnlSelection
{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
text-align: justify;
}
#pnlError
{
font-size: 0.8em;
font-weight: bold;
color: #FF0000;
}
.txtResult
{
font-weight: bold;
color: rgb(100, 100,100);
}
.hourglass
{
font-size: 5em;
}
@keyframes pulse
{
0%
{
color: rgb(255, 255, 255);
}
50%
{
color: rgb(100, 100, 100);
}
}
button
{
width: 100%;
height: 2em;
margin-top: 2em;
border-radius: 20px;
}
{
position: fixed;
display: none;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
color: rgb(255, 255, 255);
text-align: center;
animation: pulse 1s infinite;
}
#pnlContainer
{
min-height: 100vh;
max-width: 400px;
margin: 0 auto 0 auto;
text-align: center;
}
#pnlSelection
{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
text-align: justify;
}
#pnlError
{
font-size: 0.8em;
font-weight: bold;
color: #FF0000;
}
.txtResult
{
font-weight: bold;
color: rgb(100, 100,100);
}
.hourglass
{
font-size: 5em;
}
@keyframes pulse
{
0%
{
color: rgb(255, 255, 255);
}
50%
{
color: rgb(100, 100, 100);
}
}
button
{
width: 100%;
height: 2em;
margin-top: 2em;
border-radius: 20px;
}
Now, add this in the view. When the button is clicked, pnlOverlay should pop up just before the Try-catch block.
views/home.handlebars
let properties = [...selectedOptions]
.map(x => `"${x.value}"`)
.join(", ");
let prompt = `Return only a valid JSON object. The object should contain the following properties only: ${properties}. The value of each property should be the equivalent romanized version of the Chinese name "${name}" (without intonations) only. In the event of ambiguity, pick the first reasonable result.`;
document.getElementById("pnlOverlay").style.display = "block";
try {
const response = await fetch("/transliterate", {
.map(x => `"${x.value}"`)
.join(", ");
let prompt = `Return only a valid JSON object. The object should contain the following properties only: ${properties}. The value of each property should be the equivalent romanized version of the Chinese name "${name}" (without intonations) only. In the event of ambiguity, pick the first reasonable result.`;
document.getElementById("pnlOverlay").style.display = "block";
try {
const response = await fetch("/transliterate", {
And once there's a resolution, it should disappear!
views/home.handlebars
try {
const response = await fetch("/transliterate", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt: prompt
})
});
document.getElementById("pnlOverlay").style.display = "none";
const data = await response.json();
if (!data.success) {
document.getElementById("pnlError").textContent = data.error;
return;
}
const transliterations = JSON.parse(data.result);
const txtResult = document.getElementsByClassName("txtResult");
Array.from(txtResult).forEach((x) => {
let propName = x.id.replace("result_", "");
x.innerHTML = (transliterations[propName] ? transliterations[propName] : "");
});
} catch (err) {
document.getElementById("pnlError").textContent = "An unexpected error occurred.";
console.error(err);
}
const response = await fetch("/transliterate", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
prompt: prompt
})
});
document.getElementById("pnlOverlay").style.display = "none";
const data = await response.json();
if (!data.success) {
document.getElementById("pnlError").textContent = data.error;
return;
}
const transliterations = JSON.parse(data.result);
const txtResult = document.getElementsByClassName("txtResult");
Array.from(txtResult).forEach((x) => {
let propName = x.id.replace("result_", "");
x.innerHTML = (transliterations[propName] ? transliterations[propName] : "");
});
} catch (err) {
document.getElementById("pnlError").textContent = "An unexpected error occurred.";
console.error(err);
}
Here, let's try this.
There, it politely tells you to wait!
And then it shows you the results.
We're done here!
Just another fun NodeJS project. Combining my love for writing code, with exploring new human languages.Talk Too U-lei-ter!
T___T
T___T










No comments:
Post a Comment