Эх сурвалжийг харах

Refactor to add l10n, multiple fixes

Mike L 1 долоо хоног өмнө
parent
commit
90c7779cbf

+ 9 - 3
app.js

@@ -7,6 +7,8 @@ var bodyParser = require('body-parser');
 
 var index = require('./routes/index');
 var apis = require('./routes/apis');
+var basePath = require('./config.js').base_path || '/';
+var strings = require('./strings.js');
 
 var app = express();
 
@@ -20,10 +22,10 @@ app.use(logger('dev'));
 app.use(bodyParser.json({ limit: '512mb' }));
 app.use(bodyParser.urlencoded({ extended: false }));
 app.use(cookieParser());
-app.use(express.static(path.join(__dirname, 'public')));
+app.use(basePath, express.static(path.join(__dirname, 'public')));
 
 app.use('/', index);
-app.use('/api', apis);
+app.use(basePath + 'api', apis);
 
 // catch 404 and forward to error handler
 app.use(function(req, res, next) {
@@ -40,7 +42,11 @@ app.use(function(err, req, res, next) {
 
   // render the error page
   res.status(err.status || 500);
-  res.render('error');
+  var lang = (req.url.match(new RegExp('^' + basePath + '([^/]+)')) || [,])[1];
+  if (!(lang in strings)) {
+    lang = 'zh-cn';
+  }
+  res.render('error', { lang: lang, strings: strings[lang], base_path: basePath });
 });
 
 module.exports = app;

+ 4 - 5
config.js

@@ -1,7 +1,4 @@
-/**
- * naiveboom配置文件
- * @type {Object}
- */
+/* naiveboom配置文件 */
 module.exports = {
 	// redis服务器配置
 	redis_host: '127.0.0.1',
@@ -12,5 +9,7 @@ module.exports = {
 	// 1小时内单个IP可获取最多临时链接数
 	rate_limit: 500,
 	// 用于匹配GUID的正则
-	reg_guid: /(?<=\/)[a-zA-Z0-9]{8}$/,
+	reg_guid: /[a-zA-Z0-9]{8}$/,
+	// 根目录,经过反向代理放在网站子目录时配置
+	base_path: '/',
 }

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "naiveboom",
-  "version": "2.1.3",
+  "version": "2.2.0",
   "license": "MIT License",
   "private": true,
   "scripts": {

+ 18 - 10
public/javascripts/a.js

@@ -1,8 +1,10 @@
+var basePath = document.currentScript.src + '/../../';
+
 modal = new Vue({
 	el: '#modal',
 	data: {
-		msg: '么有',
-		title: '提示'
+		msg: '<none>',
+		title: '<info>'
 	},
 	methods: {
 		show: function(msg, title) {
@@ -83,12 +85,12 @@ app = new Vue({
 		},
 		getTempURL: function() {
 			$('#enurl').loading({
-				message: '加载中...',
+				message: '<loading>',
 				onStop: function(loading) {
 					loading.overlay.fadeOut(650);
 				}
 			});
-			axios.post('/api/get-temp', {
+			axios.post(basePath + 'api/get-temp', {
 				text: app.text.replace(/\]\((blob:[^)]*)\)/g, function(_, blobURL) {
 					return '](' + blobMap[blobURL] + ')';
 				})
@@ -96,17 +98,23 @@ app = new Vue({
 				$('#enurl').loading('stop');
 				var data = response.data;
 				if (data.status == 1) {
-					var newURL = location.protocol + '//' + location.host + location.pathname;
-					app.enurl = newURL + data.guid;
+					app.enurl = location.origin + location.pathname + data.guid;
 				} else {
-					throw 'failure to get the tmp url!';
-					modal.show('操作失败!<br>', '错误')
+					modal.show('<failed>', '<error>');
+					throw 'failed to get the tmp url!';
 				}
 				console.log(response);
 			}).catch(function(error) {
-				console.warn(error.response);
+				var msg;
+				if (error.code) {
+					if (error.code === 'ERR_NETWORK') msg = '<neterr>';
+					if (error.code === 'ERR_BAD_REQUEST') msg = 'Bad Request';
+				} else {
+					msg = error.response ? error.response.data.message : '<unknerr>';
+				}
+				console.warn(error);
 				$('#enurl').loading('stop');
-				modal.show('操作失败!<br>' + error.response.data.message, '错误')
+				modal.show('<failed><br><br>' + msg, '<error>')
 			})
 		}
 	}

+ 5 - 6
public/javascripts/view.js

@@ -1,3 +1,4 @@
+var basePath = document.currentScript.src + '/../../';
 var cleanURL = location.origin + location.pathname
 var origText
 
@@ -51,12 +52,10 @@ app = new Vue({
 		},
 	},
 	mounted: function() {
-		// 从URL获取GUID,然后透过POST获取消息
-		var guid = reg_guid.exec(cleanURL)[0];
-		navigator.serviceWorker.register('./sw.js').then(function () {
+		navigator.serviceWorker.register(basePath + 'sw.js').then(function () {
 			return navigator.serviceWorker.ready;
 		}).then(function() {
-			return axios.post('/api/get-msg', {
+			return axios.post(basePath + 'api/get-msg', {
 				guid: guid
 			});
 		}).then(function(response) {
@@ -81,8 +80,8 @@ app = new Vue({
 			if (status !== 1) return;
 			// 获取成功,等待装载并设定编辑器状态
 
-			this.$nextTick(() => {
-				var easyMDE = this.$refs.preview.getMDEInstance();
+			app.$nextTick(function() {
+				var easyMDE = app.$refs.preview.getMDEInstance();
 				easyMDE.togglePreview();
 				easyMDE.codemirror.setOption('readOnly', true);
 			});

+ 2 - 2
public/stylesheets/style.css

@@ -2,10 +2,10 @@ body { padding-top: 73px; font: 14px "Lucida Grande", Helvetica, Arial, sans-ser
 
 .getbtn { margin-top: -5px; margin-bottom: 10px; padding: 8px 35px; }
 
-#nb-error { font-size: 1.05em; line-height: 1.6em; font-family: Georgia, Cambria, "Times New Roman", Times, serif;}
+#error { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+#nb-error { font-size: 1.05em; line-height: 1.6em; font-family: Georgia, Cambria, "Times New Roman", Times, serif; }
 footer a { margin-right: 9px }
 footer { margin-bottom: 10px; }
-#zhao { color: red; }
 
 p.tips{ line-height: 1.8em; }
 

+ 28 - 14
routes/index.js

@@ -2,26 +2,40 @@ var express = require('ultimate-express');
 var path = require('path');
 var router = express.Router();
 var conf = require('../config.js');
-var mtool = new (require('../lib/tools.js'))(conf);// 消息操作工具
+var strings = require('../strings.js');
+
+var basePath = conf.base_path || '/';
+var baseRoute = basePath + ':lang('
+				+ Object.keys(strings).join('|') + ')?';
 
 /* GET home page. */
-router.get('/', function(req, res, next) {
-	res.render('index', { title: 'Naiveboom - 比较安全' });
+router.get(baseRoute, function(req, res, next) {
+	/** @type {keyof typeof strings} */
+	var lang = req.params.lang || 'zh-cn';
+	res.render('index', {
+		lang: lang,
+		strings: strings[lang],
+		base_path: basePath,
+	});
 });
 
 /* GET service worker. */
-router.get('/sw.js', function (req, res, next) {
-	res.sendFile(path.join(__dirname, '../public/javascripts' + req.url));
+router.get(basePath + 'sw.js', function(req, res, next) {
+	res.sendFile(path.join(__dirname, '../public/javascripts/sw.js'));
 });
 
-/**
- * 提取信息
- * @param  {[type]} req  [description]
- * @param  {[type]} res) {	var        guid [description]
- * @return {[type]}      [description]
- */
-router.get(conf.reg_guid, function(req, res) {
-	res.render('look', {title: '查看内容 - naiveBoom!', reg_guid: conf.reg_guid});
-})
+var guidSubRoute = '/:guid('
+				+ conf.reg_guid.source.replace(/\$$/, '') + ')';
+/* GET view page */
+router.get(baseRoute + guidSubRoute, function (req, res, next) {
+	/** @type {keyof typeof strings} */
+	var lang = req.params.lang || 'zh-cn';
+	res.render('look', {
+		lang: lang,
+		strings: strings[lang],
+		base_path: basePath,
+		guid: req.params.guid,
+	});
+});
 
 module.exports = router;

+ 147 - 0
strings.js

@@ -0,0 +1,147 @@
+var langs = /** @type {const} */ ([
+    'en', 'zh-cn',
+])
+
+/** @type {StringDict<LangDict<string>>} */
+var strings = {
+	layout: {
+		title: {
+			en: 'NaiveBoom - Relatively Safe',
+			'zh-cn': 'NaiveBoom - 比较安全',
+		},
+		info: {
+			en: "<b>Gosh!</b> You disabled JavaScript, please enable. <br> Otherwise this site won't work.",
+			'zh-cn': '<b>上帝啊!</b> 您禁用了JavaScript,请开启它 <br> 如果您禁止使用JavaScript,将无法使用我们的服务!',
+		},
+		guide: {
+			en: "<a href='https://www.enablejavascript.io/en' target='_blank' class='alert-link'>Find out how to enable JavaScript.</a>",
+			'zh-cn': "<a href='https://www.enablejavascript.io/cn' target='_blank' class='alert-link'>查看如何启用JavaScript</a>",
+		},
+		homepage: {
+			en: 'Home Page',
+			'zh-cn': '首页',
+		},
+	},
+	error: {
+		title: {
+			en: 'NaiveBoom - Error',
+			'zh-cn': 'NaiveBoom - 出现错误',
+		},
+		info: {
+			en: 'An error occurred on this page.',
+			'zh-cn': '出现了一些情况,页面发生错误了。',
+		},
+	},
+	index: {
+		prompt: {
+			en: "<p class='lead'>Anonymous Single-View Message</p>"
+				+ '<p>Type or paste something in the editor below,'
+				+ '<br>then press <u><b>Generate</b></u> to get a short link.</p>'
+				+ '<p>Share this link with your friend, and they only get to open it once,'
+				+ '<br>when they close the page, the message will be gone <b>forever</b>!</p>',
+			'zh-cn': "<p class='lead'>无需注册的阅后即焚</p>"
+					+ '<p>在下面的框框里输入或粘贴一些内容'
+					+ '<br>然后点一下「生成」按钮,你就会得到一个短的网址</p>'
+					+ '<p>将这个网址发给你的朋友,TA只能看一次'
+					+ '<br>关掉页面后,内容就会 <b>永远消失</b>! </p>',
+		},
+		shareprompt: {
+			en: 'Share this message with this link:',
+			'zh-cn': '可以把这个网址分享给你的朋友:',
+		},
+		generate: {
+			en: 'Generate',
+			'zh-cn': '生成',
+		},
+		info: {
+			en: "<h6>Naiveboom - Avoid naivetés"
+				+ "<a href='//en.wikipedia.org/wiki/Moha_(meme)' target='_blank' > <sup>[1]</sup></a></h6><p></p>"
+				+ '<p>Want to share a message but have concerns with it being exposed to 3<sup>rd</sup> parties?</p>'
+				+ '<p>Just paste it here (Markdown syntax and attachments supported),'
+				+ '<br>then generate a single-use link that invalidates itself after opening.</p>'
+				+ '<p>Now at the very least you would know when things goes wrong.</p>',
+			'zh-cn': '<h6>Naiveboom - 让Naive的事情不再发生</h6><p></p>'
+					+ '<p>举例:你有一段较为私密的信息需要发送,不想被第三者看到</p>'
+					+ '<p>你可以将这段文字粘贴到这个页面(支持Markdown格式和附件)'
+					+ '<br>产生一个一次性链接,阅后即焚</p>'
+					+ '<p>这样如果出事的话,至少你们能知道(笑)</p>',
+		},
+		'<close>': {
+			en: 'Close',
+			'zh-cn': '关闭',
+		},
+		'<info>': {
+			en: 'Info',
+			'zh-cn': '提示',
+		},
+		'<error>': {
+			en: 'Error',
+			'zh-cn': '错误',
+		},
+		'<none>': {
+			en: '(None)',
+			'zh-cn': '(空)',
+		},
+		'<loading>': {
+			en: 'Loading...',
+			'zh-cn': '加载中...',
+		},
+		'<failed>': {
+			en: 'Failed to perform action:',
+			'zh-cn': '操作失败!',
+		},
+		'<neterr>': {
+			en: 'Network error',
+			'zh-cn': '网络错误',
+		},
+		'<unknerr>': {
+			en: 'Unknown error',
+			'zh-cn': '未知错误',
+		},
+	},
+	look: {
+		info: {
+			en: 'Uh oh 😯, the message is already gone...',
+			'zh-cn': '啊喔😯,你要查看的内容已经不见了...',
+		},
+		tips: {
+			en: '<p>This means it has already been opened once, and the message was deleted from server.</p>'
+				+ '<p>Get it? This link is <b>single-use only!</b><p>',
+			'zh-cn': '<p>这是因为这个网址已经被看过一次了,被看过一次的内容就会自动消失</p>'
+					+ '<p>对,这就是人们常说的 <b>阅后即焚!</b></p>',
+		},
+		metoo: {
+			en: 'I want to get a link too!',
+			'zh-cn': '我也要生成一个!',
+		},
+		retrieving: {
+			en: 'Retrieving message...',
+			'zh-cn': '正在获取...',
+		},
+	}
+}
+
+/**
+ * @template T
+ * @typedef {{ [file: string]: { [key: string]: T } }} StringDict
+ */
+
+ /**
+ * @template T
+ * @typedef {{ [k in typeof langs[number]]: T }} LangDict
+ */
+
+/** @type {LangDict<StringDict<string>>} */
+var langStrings = {}
+
+langs.forEach(function (lang) {
+	langStrings[lang] = {}
+	Object.keys(strings).forEach(function(file) {
+		langStrings[lang][file] = {}
+		Object.keys(strings[file]).forEach(function(key) {
+			langStrings[lang][file][key] = strings[file][key][lang];
+		})
+	});
+});
+
+module.exports = langStrings

+ 3 - 3
views/error.pug

@@ -1,6 +1,6 @@
 extends layout
 block content
-  h1= message
-  h2= error.status
-  h3 出现了一些情况,页面发生错误了。
+  h3#error= error
+  p
+  h4= strings.error.info
   pre#nb-error #{error.stack}

+ 1 - 1
views/imports.pug

@@ -6,4 +6,4 @@ script.
 script(src='https://cdn.jsdelivr.net/npm/highlight.svelte/dist/svelte.min.js')
 link(rel='stylesheet', href='https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css')
 script(src='https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js')
-script(src='/javascripts/vue-easymde.umd.js')
+script(src=base_path+'javascripts/vue-easymde.umd.js')

+ 26 - 17
views/index.pug

@@ -4,16 +4,13 @@ block content
     <div id='app'>
         <div class='row'>
             <div class='col-md-10 col-xs-12 col-sm-12'>
-                <p> <p class='lead'>无需注册的阅后即焚</p>在下面的框框里输入一些内容
-                | <br>然后点一下「去产生」按钮,你就会得到一个长的网址
-                | <br>将这个网址发给你的朋友,他只能看一次
-                | 内容就会 <b>永远消失</b>! </p>
+                <p>!{strings.index.prompt}</p>
                 <vue-easymde v-model='text' :options='easyMDEOpts'></vue-easymde>
-                <button :disabled='!text' v-on:click='getTempURL' class='btn btn-primary getbtn'>去产生</button>
+                <button :disabled='!text' v-on:click='getTempURL' class='btn btn-primary getbtn'>!{strings.index.generate}</button>
 
                 <transition name='fade'>
                     <p v-if='enurl'>
-                        |可以把这个网址分享给你的朋友:
+                        | !{strings.index.shareprompt}
                         | <span id='enurl'>
                         | {{ enurl }}
                         | <button v-on:click='copyTempURL' class='btn btn-sm'>
@@ -22,13 +19,7 @@ block content
                     </p>
                 </transition>
                 hr
-                <p>
-                    | <h6>Naiveboom - 让Naive的事情不再发生</h6>
-                    | 举例:你有个链接或者一段文字需要发送给某人<br>
-                    | 你将私密的链接或者文字粘贴到我们这个页面<br>
-                    | 产生一个一次性链接<br>
-                    | 阅后即焚<br>
-                </p>
+                <p>!{strings.index.info}</p>
             </div>
         </div>
     </div>
@@ -38,18 +29,36 @@ block content
           <div class='modal-dialog' role='document'>
             <div class='modal-content'>
               <div class='modal-header'>
-                <h5 class='modal-title'>{{ title }}</h5>
+                <h5 class='modal-title'>{{ tr(title) }}</h5>
                 <button type='button' class='close' data-dismiss='modal' aria-label='Close'>
                   <span aria-hidden='true'>&times;</span>
                 </button>
               </div>
-              <div class='modal-body' v-html='msg'>
+              <div class='modal-body' v-html='tr(msg)'>
               </div>
               <div class='modal-footer'>
-                <button type='button' class='btn btn-primary' data-dismiss='modal'>关闭</button>
+                <button type='button' class='btn btn-primary' data-dismiss='modal'>{{ tr('&lt;close&gt;') }}</button>
               </div>
             </div>
           </div>
         </div>
     </div>
-    script(src='/javascripts/a.js')
+    //- Export translations for modal
+    script.
+        var dynstrs = {
+            '<close>':  '!{strings.index["<close>"]}',
+            '<info>':   '!{strings.index["<info>"]}',
+            '<error>':  '!{strings.index["<error>"]}',
+            '<none>':   '!{strings.index["<none>"]}',
+            '<loading>':'!{strings.index["<loading>"]}',
+            '<failed>': '!{strings.index["<failed>"]}',
+            '<neterr>': '!{strings.index["<neterr>"]}',
+            '<unknerr>':'!{strings.index["<unknerr>"]}'
+        };
+        tr = function(str) {
+            var test = new RegExp(Object.keys(dynstrs).join('|'), 'g');
+            return str.replace(test, function(key) {
+                return dynstrs[key];
+            })
+        };
+    script(src=base_path+'javascripts/a.js')

+ 11 - 12
views/layout.pug

@@ -1,21 +1,21 @@
 doctype html
-html
+html(lang= lang)
   head
-    title= title
+    title= strings.layout.title
     link(rel='stylesheet', href='https://cdn.jsdelivr.net/npm/bootstrap@4/dist/css/bootstrap.min.css')
     script(src='https://cdn.jsdelivr.net/npm/jquery@3.5/dist/jquery.min.js')
     script(src='https://cdn.jsdelivr.net/npm/bootstrap@4/dist/js/bootstrap.bundle.min.js')
     script(src='https://cdn.jsdelivr.net/npm/vue@2')
     script(src='https://cdn.jsdelivr.net/npm/axios@0')
-    script(src='/javascripts/jquery.loading.min.js')
+    script(src=base_path+'javascripts/jquery.loading.min.js')
 
-    link(rel='stylesheet', href='/stylesheets/style.css')
-    link(rel='stylesheet', href='/stylesheets/jquery.loading.min.css')
+    link(rel='stylesheet', href=base_path+'stylesheets/style.css')
+    link(rel='stylesheet', href=base_path+'stylesheets/jquery.loading.min.css')
     meta(content='width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0' name='viewport')
   body
     <div class='navbar navbar-expand-lg fixed-top navbar-light bg-light'>
           <div class='container'>
-            <a href='/' class='navbar-brand'>naiveBoom</a>
+            <a href='!{base_path}!{lang}' class='navbar-brand'>naiveBoom</a>
             <button class='navbar-toggler' type='button' data-toggle='collapse' data-target='#navbarResponsive' aria-controls='navbarResponsive' aria-expanded='false' aria-label='Toggle navigation' style=''>
               <span class='navbar-toggler-icon'></span>
             </button>
@@ -33,16 +33,15 @@ html
     </div>
     <div class='container maincontent'>
       <noscript>
-        <div class='alert alert-dismissible alert-danger'>
-          <button type='button' class='close' data-dismiss='alert'>&times;</button>
-          <strong>上帝啊!</strong> 您禁用了JavaScript,请开启它 <br> 如果您禁止使用JavaScript,将无法使用我们的服务!
-          <a href='http://www.chiefgroup.com.hk/temp/chiefeasy/how-to-enable-javascript.html' target='_blank' class='alert-link'>查看如何开启Cookie</a>
-        </div>
+        div.alert.alert-dismissible.alert-danger
+          | <button type='button' class='close' data-dismiss='alert'>&times;</button>
+          | !{strings.layout.guide}
+          | !{strings.layout.info}
       </noscript>
       block content
     </div>
     hr
     footer
       .container
-         a(href='/') 首页
+         a(href=base_path+lang) !{strings.layout.homepage}
          a(href='https://github.com/kchown/naiveboom' target='_blank') GitHub

+ 8 - 9
views/look.pug

@@ -6,16 +6,15 @@ block content
 			<vue-easymde ref='preview' :value='text' :options='easyMDEOpts'></vue-easymde>
 		</div>
 		<div v-else-if='status==0'>
-		h6 啊喔😯,你查看的内容已经不见了...
-		p.tips
-			| 这是因为这个网址已经被看过一次了,被看过一次的内容就会自动消失
-			| <br>对,这就是人们常说的 <b>阅后即焚!</b>
-		a(href='/') 我也要生成一个!
+		h5= strings.look.info
+		p
+		| !{strings.look.tips}
+		a(href='.')= strings.look.metoo
 		</div>
 		<div v-else>
-		p#warning 正在获取...
+		p#warning= strings.look.retrieving
 		</div>
-	//- 输出用于匹配GUID的正则表达式
+	//- Export GUID matching regex for Vue
 	script.
-		reg_guid =  !{locals.reg_guid};
-	script(src='/javascripts/view.js')
+		guid = '!{locals.guid}';
+	script(src=base_path+'javascripts/view.js')