UNPKG

5.77 kBJavaScriptView Raw
1/*
2################################################################################
3# #
4# db db .8888. dP 888888b 8888ba .8888. d8b db 888888b d8888P #
5# 88 88 d8' `8b 88 88 88 `8b d8' `8b 88V8 88 88 88 #
6# Y8 8P 88 88 88 a88aaa 88aa8P' 88 88 88 V8 88 88aaa 88 #
7# `8b d8' 88 88 88 88 88 `8b 88 88 88 V888 88 88 #
8# `8bd8' Y8. .8P 88 88 88 .88 Y8. .8P dP 88 V88 88 88 #
9# YP `888P' 88888P 888888P 888888' `888P' 88 VP 8P 888888P dP #
10# #
11################################################################################
12
13Language-helper middleware for Express web server.
14
15Copyright (C) 2016-2017 Volebo <dev@volebo.net>
16Copyright (C) 2016-2017 Maksim Koryukov <maxkoryukov@gmail.com>
17
18This program is free software: you can redistribute it and/or modify
19it under the terms of the MIT License, attached to this software package.
20
21This program is distributed in the hope that it will be useful,
22but WITHOUT ANY WARRANTY; without even the implied warranty of
23MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
24
25You should have received a copy of the MIT License along with this
26program. If not, see <https://opensource.org/licenses/MIT>.
27*/
28
29'use strict'
30
31const debug = require('debug')('volebo:express:mw:lang')
32const _ = require('lodash')
33const express = require('express')
34const deepFreeze = require('deep-freeze')
35
36const urlBuilder = require('url').parse
37
38const knownLangs = require('./known-langs')
39// dict with langs structures,
40//
41// keys : ISO 639-x codes (lowercase)
42// values : looks like this:
43//
44// {
45// code: 'en',
46// name: {
47// short: 'en',
48// full: 'English',
49// native: {
50// short: 'en',
51// full: 'English'
52// }
53// }
54// }
55
56
57const LangMw = function(options) {
58
59 const _opt = options || {}
60
61 let defLangCode = _.toString(_opt.defaultLanguage) || 'en'
62 const defLangData = knownLangs[_.toLower(defLangCode)]
63
64 if (!defLangData) {
65 throw new Error(`Unknown defaultLanguage: [${defLangCode}]`)
66 }
67
68 defLangCode = defLangData.code
69
70 const available_lang_codes = _opt.availableLanguages || []
71 available_lang_codes.push(defLangCode)
72
73 const _available = _(available_lang_codes)
74 .uniq()
75 .map(lcode => knownLangs[_.toLower(lcode)])
76 .filter()
77 .value()
78 const availableFrozen = deepFreeze(_available)
79 const _availableDict = _.keyBy(availableFrozen, v => _.toLower(v.code))
80
81 const _onLangDetected = _opt.onLangDetected || _.noop
82
83 if (!_.isFunction(_onLangDetected)) {
84 throw new TypeError('options.onLangDetected MUST be a function')
85 }
86
87 // -------------------------------------------------------------------------
88 // OPTIONS PARSED
89 // -------------------------------------------------------------------------
90 // debug('defaultLanguage', defLangCode)
91 // debug('availableFrozen', availableFrozen)
92
93 const mw_lang_pre_handler = function mw_lang_pre_handler(req, res, next) {
94 let lang_code = _.get(req.params, 'lang')
95 let is_defLangCode
96
97 if (_.isNil(lang_code)) {
98 is_defLangCode = true
99 lang_code = defLangCode
100 } else {
101 is_defLangCode = false
102 const cult_code = _.get(req.params, 'cult', '')
103 lang_code = lang_code + cult_code
104 }
105
106 let lc = _availableDict[_.toLower(lang_code)]
107
108 if (!lc){
109 // TODO: is this solution slow?
110
111 const _urlPathname = urlBuilder(req.originalUrl).pathname
112
113 // if the `originalUrl` ends with a slash - everything will work
114 // just fine.
115 // but if the `originalUrl` is something like [/me] or [/vk-ME]
116 // than express-url-trimmer will do bad work for us.
117 // To avoid this it is enough to add an additional slash at the
118 // begging of req.url
119 //
120 // HACK: but this solution could be considered as a hack!
121 //
122 const _prependSlash = '/' !== _urlPathname[_urlPathname.length-1]
123
124 const _newUrl = _prependSlash ? '/' + req.originalUrl : req.originalUrl
125 debug('use default culture and restore URL to original value [%s]', _newUrl)
126 req.url = _newUrl
127
128 lc = defLangData
129 is_defLangCode = true
130 }
131
132 const localeinfo = {}
133
134 localeinfo.defaultLanguage = defLangCode
135 localeinfo.available = availableFrozen
136 localeinfo.usingDefault = is_defLangCode
137
138 localeinfo.routeTo = function _routeTo(local_path, explicit_lang_code) {
139 if (_.isNil(explicit_lang_code)) {
140 explicit_lang_code = localeinfo.code
141 }
142
143 // TODO: it could be precalculated
144 // validate explicit_lang_code:
145 const lang = _availableDict[_.toLower(explicit_lang_code)]
146 const effective_lang_code = lang ? lang.code : defLangCode
147
148 let p = local_path
149 if (effective_lang_code !== defLangCode) {
150 p = `/${effective_lang_code}${local_path}`
151 }
152
153 return p
154 }
155
156 localeinfo.setLocale = function _setLocale(lc) {
157
158 const new_lc = _availableDict[_.toLower(lc)]
159
160 if (!new_lc) {
161 throw new Error(`Locale not found: ${lc}`)
162 }
163
164 _.assign(this, new_lc)
165
166 _onLangDetected(new_lc.code, req, res)
167 }
168
169 // Set up req and res:
170 res.locals.lang = localeinfo
171 req.lang = localeinfo
172
173 localeinfo.setLocale(lc.code)
174
175 return next()
176 }
177
178 const _router = new express.Router()
179 _router.available = availableFrozen
180 _router.defaultLanguage = defLangCode
181
182 _router.esu = function(app) {
183 //
184 app.use('/:lang(\\w{2})?:cult([-_]\\w{2,3})?', mw_lang_pre_handler, _router)
185 }
186
187 return _router
188}
189
190exports = module.exports = LangMw