diff --git a/exec/default.js b/exec/default.js
index 65abebeb383cd75ed961f1025b00db5f48f17db5..afb39a84b09e648775aa44cf425e4b5c5918b0da 100755
--- a/exec/default.js
+++ b/exec/default.js
@@ -9,6 +9,7 @@ require("sbbsdefs.js", "K_UPPER");
 require("userdefs.js", "UFLAG_T");
 require("nodedefs.js", "NODE_MAIN");
 require("key_defs.js", "KEY_UP");
+require("gettext.js", "gettext");
 require("text.js", "Pause");
 bbs.revert_text(Pause);
 load("termsetup.js");
@@ -22,7 +23,7 @@ const main_menu = {
 	file: "main",
 	eval: 'bbs.main_cmds++',
 	node_action: NODE_MAIN,
-	prompt: "\x01-\x01c\xfe \x01b\x01hMain \x01n\x01c\xfe \x01h" + time_code +
+	prompt: "\x01-\x01c\xfe \x01b\x01h" + gettext("Main") + " \x01n\x01c\xfe \x01h" + time_code +
 		" \x01n\x01c[\x01h@GN@\x01n\x01c] @GRP@\x01\\ [\x01h@SN@\x01n\x01c] @SUB@: \x01n",
 	num_input: shell.get_sub_num,
 	slash_num_input: shell.get_grp_num,
@@ -30,14 +31,14 @@ const main_menu = {
 	 'A': { eval: 'bbs.auto_msg()' },
 	'/A': { exec: 'avatar_chooser.js'
 			,ars: 'ANSI and not GUEST'
-			,err: '\r\nSorry, only regular users with ANSI terminals can do that.\r\n' },
+			,err: '\r\n' + gettext("Sorry, only regular users with ANSI terminals can do that.") + '\r\n' },
 	 'B': { eval: 'bbs.scan_subs(SCAN_BACK)'
-			,msg: '\r\n\x01c\x01hBrowse/New Message Scan\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("Browse/New Message Scan") + '\r\n' },
 	 'C': { eval: 'bbs.chat_sec()' },
 	 'D': { eval: 'bbs.user_config(); exit()' },
 	 'E': { exec: 'email_sec.js' },
 	 'F': { eval: 'bbs.scan_subs(SCAN_FIND)'
-			,msg: '\r\n\x01c\x01hFind Text in Messages\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("Find Text in Messages") + '\r\n' },
 	'/F': { eval: 'bbs.scan_subs(SCAN_FIND, /* all */true)' },
 	 'G': { eval: 'bbs.text_sec()' },
 	 'I': { eval: 'shell.main_info()' },
@@ -46,7 +47,7 @@ const main_menu = {
 	'/L': { eval: 'bbs.list_nodes()' },
 	 'M': { eval: 'bbs.time_bank()' },
 	 'N': { eval: 'bbs.scan_subs(SCAN_NEW)'
-			,msg: '\r\n\x01c\x01hNew Message Scan\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("New Message Scan") + '\r\n' },
 	'/N': { eval: 'bbs.scan_subs(SCAN_NEW, /* all */true)' },
 	 'O': { eval: 'shell.logoff(/* fast: */false)' },
 	'/O': { eval: 'shell.logoff(/* fast: */true)' },
@@ -55,7 +56,7 @@ const main_menu = {
 	 'Q': { eval: 'bbs.qwk_sec()' },
 	 'R': { eval: 'bbs.scan_msgs()' },
 	 'S': { eval: 'bbs.scan_subs(SCAN_TOYOU)'
-			,msg: '\r\n\x01c\x01hScan for Messages Posted to You\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("Scan for Messages Posted to You") + '\r\n' },
 	'/S': { eval: 'bbs.scan_subs(SCAN_TOYOU, /* all */true)' },
 	 'U': { eval: 'shell.list_users()' },
 	'/U': { eval: 'bbs.list_users(UL_ALL)' },
@@ -64,15 +65,15 @@ const main_menu = {
 	 'W': { eval: 'bbs.whos_online()' },
 	 'X': { eval: 'bbs.xtrn_sec()' },
 	 'Z': { eval: 'bbs.scan_subs(SCAN_NEW | SCAN_CONT)'
-			,msg: '\r\n\x01c\x01hContinuous New Message Scan\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("Continuous New Message Scan") + '\r\n' },
 	'/Z': { eval: 'bbs.scan_subs(SCAN_NEW | SCAN_CONT, /* all */true)' },
 	 '*': { eval: 'shell.show_subs(bbs.curgrp)' },
 	'/*': { eval: 'shell.show_grps()' },
 	 '&': { exec: 'msgscancfg.js' },
 	 '!': { eval: 'bbs.menu("sysmain")'
 			,ars: 'SYSOP or EXEMPT Q or I or N' },
-	 '#': {  msg: '\r\n\x01c\x01hType the actual number, not the symbol.\r\n' },
-	'/#': {  msg: '\r\n\x01c\x01hType the actual number, not the symbol.\r\n' },
+	 '#': {  msg: '\r\n\x01c\x01h' + gettext("Type the actual number, not the symbol.") + '\r\n' },
+	'/#': {  msg: '\r\n\x01c\x01h' + gettext("Type the actual number, not the symbol.") + '\r\n' },
 	},
 	nav: {
 	'\r': { },
@@ -101,7 +102,7 @@ const file_menu = {
 	file: "transfer",
 	eval: 'bbs.file_cmds++',
 	node_action: NODE_XFER,
-	prompt: "\x01-\x01c\xfe \x01b\x01hFile \x01n\x01c\xfe \x01h" + time_code +
+	prompt: "\x01-\x01c\xfe \x01b\x01h" + gettext("File") + " \x01n\x01c\xfe \x01h" + time_code +
 		" \x01n\x01c(\x01h@LN@\x01n\x01c) @LIB@\x01\\ (\x01h@DN@\x01n\x01c) @DIR@: \x01n",
 	num_input: shell.get_dir_num,
 	slash_num_input: shell.get_lib_num,
@@ -109,46 +110,46 @@ const file_menu = {
 	 'B': { eval: 'bbs.batch_menu()' },
 	 'C': { eval: 'bbs.chat_sec()' },
 	 'D': { eval: 'shell.download_files()'
-			,msg: '\r\n\x01c\x01hDownload File(s)\r\n'
+			,msg: '\r\n\x01c\x01h' + gettext("Download File(s)") + '\r\n'
 			,ars: 'REST NOT D' },
 	'/D': { eval: 'shell.download_user_files()'
-			,msg: '\r\n\x01c\x01hDownload File(s) from User(s)\r\n'
+			,msg: '\r\n\x01c\x01h' + gettext("Download File(s) from User(s)") + '\r\n'
 			,ars: 'REST NOT D' },
 	 'E': { eval: 'shell.view_file_info(FI_INFO)'
-			,msg: '\r\n\x01c\x01hList Extended File Information\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("List Extended File Information") + '\r\n' },
 	 'F': { eval: 'bbs.scan_dirs(FL_FINDDESC);'
-			,msg: '\r\n\x01c\x01hFind Text in File Descriptions (no wildcards)\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("Find Text in File Descriptions (no wildcards)") + '\r\n' },
 	'/F': { eval: 'bbs.scan_dirs(FL_FINDDESC, /* all: */true);' },
 	 'I': { eval: 'shell.file_info()' },
 	 'J': { eval: 'shell.select_file_area()' },
 	 'L': { eval: 'shell.list_files()' },
 	'/L': { eval: 'bbs.list_nodes()' },
 	 'N': { eval: 'bbs.scan_dirs(FL_ULTIME)'
-			,msg: '\r\n\x01c\x01hNew File Scan\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("New File Scan") + '\r\n' },
 	'/N': { eval: 'bbs.scan_dirs(FL_ULTIME, /* all */true)' },
 	 'O': { eval: 'shell.logoff(/* fast: */false)' },
 	'/O': { eval: 'shell.logoff(/* fast: */true)' },
 	 'R': { eval: 'shell.view_file_info(FI_REMOVE)'
-			,msg: '\r\n\x01c\x01hRemove/Edit File(s)\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("Remove/Edit File(s)") + '\r\n' },
 	 'S': { eval: 'bbs.scan_dirs(FL_NO_HDR)'
-			,msg: '\r\n\x01c\x01hSearch for Filename(s)\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("Search for Filename(s)") + '\r\n' },
 	'/S': { eval: 'bbs.scan_dirs(FL_NO_HDR, /* all */true) ' },
 	 'T': { eval: 'bbs.temp_xfer()' },
 	 'U': { eval: 'shell.upload_file()'
-			,msg: '\r\n\x01c\x01hUpload File\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("Upload File") + '\r\n' },
 	'/U': { eval: 'shell.upload_user_file()'
-			,msg: '\r\n\x01c\x01hUpload File to User\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("Upload File to User") + '\r\n' },
 	 'V': { eval: 'shell.view_files()'
-			,msg: '\r\n\x01c\x01hView File(s)\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("View File(s)") + '\r\n' },
 	 'W': { eval: 'bbs.whos_online()' },
 	 'Z': { eval: 'shell.upload_sysop_file()'
-			,msg: '\r\n\x01c\x01hUpload File to Sysop\r\n' },
+			,msg: '\r\n\x01c\x01h' + gettext("Upload File to Sysop") + '\r\n' },
 	 '*': { eval: 'shell.show_dirs(bbs.curlib)' },
 	'/*': { eval: 'shell.show_libs()' },
 	 '&': { exec: 'filescancfg.js' },
 	 '!': { eval: 'bbs.menu("sysxfer")' },
-	 '#': {  msg: '\r\n\x01c\x01hType the actual number, not the symbol.\r\n' },
-	'/#': {  msg: '\r\n\x01c\x01hType the actual number, not the symbol.\r\n' },
+	 '#': {  msg: '\r\n\x01c\x01h' + gettext("Type the actual number, not the symbol.") + '\r\n' },
+	'/#': {  msg: '\r\n\x01c\x01h' + gettext("Type the actual number, not the symbol.") + '\r\n' },
 	},
 	nav: {
 	'\r': { },
@@ -232,9 +233,9 @@ while(bbs.online && !js.terminated) {
 	}
 	var menu_cmd = menu.command[cmd];
 	if(!menu_cmd) {
-		console.print("\r\n\x01c\x01hUnrecognized command.");
+		console.print("\r\n\x01c\x01h" + gettext("Unrecognized command."));
 		if(user.settings & USER_EXPERT)
-			console.print(" Hit '\x01i" + help_key + "\x01n\x01c\x01h' for a menu.");
+			console.print(" " + gettext("Hit") + " '\x01i" + help_key + "\x01n\x01c\x01h' " + gettext("for a menu."));
 		console.crlf();
 		continue;
 	}
diff --git a/exec/load/gettext.js b/exec/load/gettext.js
new file mode 100755
index 0000000000000000000000000000000000000000..dbd7401f80be07eb7c7d5976cf12d223baf033b9
--- /dev/null
+++ b/exec/load/gettext.js
@@ -0,0 +1,30 @@
+// Localized/customized support for JavaScript user-visible text (i.e. strings not contained in text.dat)
+// Customized text strings go in the [JS] section of ctrl/text.ini
+// Localized (translated to non-default locale) text strings go in the [JS] section of ctrl/text.<lang>.ini
+
+"use strict";
+
+var gettext_cache = {};
+
+function gettext(orig) {
+	function get_text_from_ini(ini_fname, orig)	{
+		var f = new File(ini_fname);
+		if(!f.open("r"))
+			return undefined;
+		var text = f.iniGetValue("js", orig);
+		f.close();
+		return text;
+	}
+	if (gettext_cache[orig] !== undefined)
+		return gettext_cache[orig];
+	var text;
+	if (user.lang)
+		text = get_text_from_ini("text." + user.lang + ".ini", orig);
+	if (text === undefined)
+		text = get_text_from_ini("text.ini", orig);
+	if (text === undefined)
+		text = orig;
+	return gettext_cache[orig] = text;
+}
+
+this;
diff --git a/exec/load/shell_lib.js b/exec/load/shell_lib.js
index 80414cdb8dfd1696c5c935bfaed98ce04bbba70f..e322957f9974851230d788970fc3fefbc53a02e8 100755
--- a/exec/load/shell_lib.js
+++ b/exec/load/shell_lib.js
@@ -7,6 +7,7 @@
 
 require("text.js", "DownloadBatchQ");
 require("sbbsdefs.js", "USER_EXPERT");
+require("gettext.js", "gettext");
 
 // Build list of current subs/dirs in each group/library
 // This hack is required because the 'bbs' object doesn't expose the current
@@ -244,7 +245,7 @@ function main_info()
 		if(!(user.settings & USER_EXPERT))
 			bbs.menu("maininfo");
 		bbs.nodesync();
-		console.print("\r\n\x01y\x01hInfo: \x01n");
+		console.print("\r\n\x01y\x01h"+ gettext("Info") + ": \x01n");
 		var key = console.getkeys("?QISVY\r");
 		bbs.log_key(key);
 		switch(key) {
@@ -290,7 +291,7 @@ function file_info()
 		if(!(user.settings & USER_EXPERT))
 			bbs.menu("xferinfo");
 		bbs.nodesync();
-		console.print("\r\n\x01y\x01hInfo: \x01n");
+		console.print("\r\n\x01y\x01h" + gettext("Info") + ": \x01n");
 		key=console.getkeys("?TYDUQ\r");
 		bbs.log_key(key);
 
@@ -325,9 +326,9 @@ function file_info()
 
 function list_users()
 {
-	console.print("\r\n\x01c\x01hList Users\r\n");
-	console.mnemonics("\r\n~Logons Today, ~Yesterday, ~Sub-board, or ~All: ");
-	switch(console.getkeys("LSAY\r")) {
+	console.print("\r\n\x01c\x01h" + "List Users" + "\r\n");
+	console.mnemonics("\r\n~" + gettext("Logons Today") + ", ~" + gettext("Yesterday") + ", ~" + gettext("Sub-board") + ", " + gettext("or") + " ~@All@: ");
+	switch(console.getkeys("LSY\r" + console.all_key)) {
 	case 'L':
 		bbs.list_logons();
 		break;
@@ -337,7 +338,7 @@ function list_users()
 	case 'S':
 		bbs.list_users(UL_SUB);
 		break;
-	case 'A':
+	case console.all_key:
 		bbs.list_users(UL_ALL);
 		break;
 	}