Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Pluggable storage engine, simplify code

  • Loading branch information...
commit 2571ffb584b20eb49eacf9c1caa54567a2aa16dc 1 parent 2e06f77
@ricardobeat authored
View
158 lib/i18n.js
@@ -1,5 +1,5 @@
(function() {
- var debug, fs, i18n, path, _,
+ var fs, i18n, path, store, _,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
path = require('path');
@@ -8,25 +8,27 @@
_ = require('./util');
- debug = function(str) {
- return i18n.options.debug && console.log("[i18n] " + str);
- };
+ store = require('./store');
i18n = function(options) {
- i18n.options = _.extend({
+ i18n.options = options = _.extend({
"default": 'en',
path: '/lang',
- debug: false
+ debug: false,
+ store: 'json'
}, options);
- i18n.languages.push(i18n.options["default"]);
+ _.toggleDebug(options.debug);
+ i18n.store = new store[options.store](i18n);
+ i18n.languages.push(options["default"]);
i18n.loadLanguageFiles();
return function(req, res, next) {
- var locale, _ref;
+ var _ref;
+ if (req.path === '/favicon.ico') {
+ return next();
+ }
if (((_ref = req.session) != null ? _ref.lang : void 0) == null) {
- locale = i18n.getLocale(req);
- req.session.locale = locale;
- req.session.lang = locale.slice(0, 3);
- debug("Language set to " + req.session.lang);
+ i18n.setLanguage(req.session, i18n.getLocale(req));
+ _.debug("Language set to " + req.session.lang);
}
res.locals({
locale: req.session.locale,
@@ -46,59 +48,6 @@
languages: i18n.languages
};
- i18n.loadLanguageFiles = function() {
- var data, dir, filePath, files, locale, _i, _len, _results;
- dir = i18n.options.path;
- if (fs.existsSync(process.cwd() + dir)) {
- files = fs.readdirSync(process.cwd() + dir).map(function(f) {
- return path.basename(f, '.json');
- }).filter(_.isValidLocale);
- _results = [];
- for (_i = 0, _len = files.length; _i < _len; _i++) {
- locale = files[_i];
- if (!(locale !== i18n.options["default"])) {
- continue;
- }
- filePath = path.join(process.cwd(), dir, locale + '.json');
- try {
- data = JSON.parse(fs.readFileSync(filePath).toString());
- i18n.strings[locale] = data;
- i18n.languages.push(locale);
- _results.push(debug("loaded " + locale + ".json"));
- } catch (e) {
- _results.push(debug("failed to load language file " + filePath));
- }
- }
- return _results;
- } else {
- return debug("path " + dir + " doesn't exist");
- }
- };
-
- i18n.isValidLocale = function(locale) {
- return /^\w\w(-\w\w)?$/.test(locale);
- };
-
- i18n.getLocale = function(req) {
- var acceptHeader, languages, locale, _i, _len;
- languages = [];
- if (acceptHeader = req.header('Accept-Language')) {
- languages = acceptHeader.split(/,|;/g).filter(i18n.isValidLocale);
- }
- if (languages.length < 1) {
- languages.push(i18n.options["default"]);
- debug("Empty Accept-Language header, reverting to default");
- }
- for (_i = 0, _len = languages.length; _i < _len; _i++) {
- locale = languages[_i];
- if (i18n.languages[locale]) {
- locale = locale.toLowerCase();
- }
- }
- locale || (locale = languages[0]);
- return locale;
- };
-
i18n.plural = function(str, zero, one, more) {
var word, _ref;
if (typeof more !== 'string') {
@@ -122,50 +71,55 @@
if (!isNaN(str) && arguments.length > 2) {
return i18n.plural.apply(this, arguments);
}
- localStrings = i18n.strings[this.locale] || i18n.strings[this.lang];
- localStrings && ((_ref = localStrings[str]) != null ? _ref : localStrings[str] = '');
- return (localStrings != null ? localStrings[str] : void 0) || str || '';
+ if (localStrings = i18n.strings[this.lang]) {
+ if ((_ref = localStrings[str]) == null) {
+ localStrings[str] = '';
+ }
+ return localStrings[str] || str;
+ } else {
+ return str || '';
+ }
};
- i18n.setLanguage = function(session, lang) {
- if (__indexOf.call(i18n.languages, lang) >= 0) {
- session.lang = lang;
- session.langbase = lang.substring(0, 2);
- return debug("Language set to " + lang);
+ i18n.setLanguage = function(session, locale) {
+ var lang;
+ lang = locale.split('-')[0];
+ if (session != null) {
+ session.lang = (function() {
+ switch (true) {
+ case __indexOf.call(i18n.languages, locale) >= 0:
+ return locale;
+ case __indexOf.call(i18n.languages, lang) >= 0:
+ return lang;
+ default:
+ return i18n.options["default"];
+ }
+ })();
}
+ return _.debug("Language set to " + (session != null ? session.lang : void 0));
};
- i18n.updateStrings = function(req, res, next) {
- var basePath, file, filePath, lang, strings, _ref;
- basePath = path.join(process.cwd(), i18n.options.path);
- _ref = i18n.strings;
- for (lang in _ref) {
- strings = _ref[lang];
- if (!(i18n.isValidLocale(lang))) {
- continue;
+ i18n.loadLanguageFiles = function() {
+ var _this = this;
+ return this.store.load(function(err, strings) {
+ return _.extend(_this.strings, strings);
+ });
+ };
+
+ i18n.getLocale = function(req) {
+ var lang, locale, _i, _len, _ref;
+ _ref = req.acceptedLanguages;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ lang = _ref[_i];
+ if (i18n.languages[locale] != null) {
+ locale = lang;
}
- file = "" + lang + ".json";
- filePath = path.join(basePath, file);
- fs.readFile(filePath, function(err, res) {
- var contents, s, t;
- try {
- contents = JSON.parse(res.toString());
- } catch (e) {
- contents = {};
- } finally {
- for (s in strings) {
- t = strings[s];
- if (contents[s]) {
- i18n.strings[lang][s] = contents[s];
- } else {
- contents[s] = t;
- }
- }
- }
- fs.writeFile(filePath, JSON.stringify(contents, null, 4), 'utf8');
- return debug("Updated strings in " + file);
- });
}
+ return (locale || i18n.options["default"]).toLowerCase();
+ };
+
+ i18n.updateStrings = function(req, res, next) {
+ i18n.store.update(i18n.strings);
return next();
};
View
92 lib/store.js
@@ -0,0 +1,92 @@
+(function() {
+ var JSONStore, fs, path, _,
+ __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+
+ path = require('path');
+
+ fs = require('fs');
+
+ _ = require('./util');
+
+ require('colors');
+
+ JSONStore = (function() {
+
+ function JSONStore(i18n) {
+ if (!(this instanceof JSONStore)) {
+ return new JSONStore;
+ }
+ this.path = path.join(process.cwd(), i18n.options.path);
+ this["default"] = i18n.options["default"];
+ this.languages = i18n.languages;
+ }
+
+ JSONStore.prototype.update = function(strings) {
+ var _this = this;
+ return this.load(function(err, stored) {
+ if (stored == null) {
+ stored = {};
+ }
+ _.extend(strings, stored);
+ return _this.save(strings, function(err, language) {
+ return _.debug(("Updated " + language + " strings").blue);
+ });
+ });
+ };
+
+ JSONStore.prototype.save = function(strings, callback) {
+ var data, language, _results,
+ _this = this;
+ _results = [];
+ for (language in strings) {
+ data = strings[language];
+ _results.push((function(language) {
+ var file;
+ file = path.join(_this.path, "" + language + ".json");
+ return fs.writeFile(file, JSON.stringify(data, null, 4), 'utf8', function(err) {
+ return callback(err, language);
+ });
+ })(language));
+ }
+ return _results;
+ };
+
+ JSONStore.prototype.load = function(callback) {
+ var file, files, locale, strings, _i, _len;
+ strings = {};
+ if (!fs.existsSync(this.path)) {
+ _.debug(("Path '" + this.path + "' doesn't exist").red);
+ return;
+ }
+ files = fs.readdirSync(this.path).map(function(f) {
+ return path.basename(f, '.json');
+ }).filter(_.isValidLocale);
+ for (_i = 0, _len = files.length; _i < _len; _i++) {
+ locale = files[_i];
+ if (!(locale !== this["default"])) {
+ continue;
+ }
+ if (__indexOf.call(this.languages, locale) < 0) {
+ this.languages.push(locale);
+ }
+ file = path.join(this.path, "" + locale + ".json");
+ try {
+ strings[locale] = JSON.parse(fs.readFileSync(file).toString());
+ _.debug(("Loaded " + locale + ".json").blue);
+ } catch (e) {
+ strings[locale] = {};
+ _.debug(("Failed to load file " + locale + ".json").red);
+ }
+ }
+ return callback(null, strings);
+ };
+
+ return JSONStore;
+
+ })();
+
+ module.exports = {
+ json: JSONStore
+ };
+
+}).call(this);
View
12 lib/util.js
@@ -1,10 +1,10 @@
(function() {
- var debugMode,
+ var debugMode, util,
__slice = [].slice;
debugMode = false;
- module.exports = {
+ util = {
extend: function() {
var key, obj, source, sources, val, _i, _len;
obj = arguments[0], sources = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
@@ -12,7 +12,11 @@
source = sources[_i];
for (key in source) {
val = source[key];
- obj[key] = val;
+ if (typeof val === 'object' && typeof obj[key] === 'object') {
+ util.extend(obj[key], val);
+ } else {
+ obj[key] = val;
+ }
}
}
return obj;
@@ -37,4 +41,6 @@
}
};
+ module.exports = util;
+
}).call(this);
View
169 src/i18n.coffee
@@ -2,35 +2,37 @@ path = require 'path'
fs = require 'fs'
_ = require './util'
-
-# Log messages, enable with option `debug: true`.
-debug = (str) ->
- i18n.options.debug && console.log "[i18n] #{str}"
+store = require './store'
# Polyglot
-# ------------
-# Sets options, loads language files and returns an express middleware function.
+# ---------
i18n = (options) ->
# Options
- i18n.options = _.extend {
+ i18n.options = options = _.extend {
default: 'en'
path: '/lang'
debug: false
+ store: 'json'
}, options
+
+ _.toggleDebug(options.debug)
+
+ # Instantiate storage engine
+ i18n.store = new store[options.store] i18n
- i18n.languages.push i18n.options.default
+ i18n.languages.push options.default
i18n.loadLanguageFiles()
return (req, res, next) ->
+ return next() if req.path is '/favicon.ico'
+
# User doesn't have a language setting yet
unless req.session?.lang?
- locale = i18n.getLocale req
- req.session.locale = locale
- req.session.lang = locale[0..2]
- debug "Language set to #{req.session.lang}"
+ i18n.setLanguage req.session, i18n.getLocale req
+ _.debug "Language set to #{req.session.lang}"
# Register template locals
res.locals
@@ -39,89 +41,26 @@ i18n = (options) ->
next()
-# Languages list
-# ---------------
-# List of active languages, excluding `i18n.options.default`.
+# List of active languages.
i18n.languages = []
-
-# Translation strings
-# --------------------
-# In-memory strings. They are flushed to disk using `i18n.updateStrings`.
-
+# In-memory translations. Updated and flushed to disk when using `i18n.updateStrings`.
i18n.strings = {}
-
-# Default template locals
-# ------------------------
-# `lang` and `locale` are also set on a per-request basis (see middleware function).
-
+# Default template locals. `lang` and `locale` are also set
+# for each request (see middleware function).
i18n.locals = {
__ : i18n.translate
_n : i18n.plural
languages : i18n.languages
}
-# Language files loader
-# ----------------------
-# Load `json` language files on startup.
-
-i18n.loadLanguageFiles = ->
- dir = i18n.options.path
-
- if fs.existsSync(process.cwd() + dir)
- files = fs.readdirSync(process.cwd() + dir)
- .map((f) -> path.basename f, '.json')
- .filter _.isValidLocale
-
- for locale in files when locale isnt i18n.options.default
- filePath = path.join process.cwd(), dir, locale + '.json'
- try
- data = JSON.parse fs.readFileSync(filePath).toString()
- i18n.strings[locale] = data
- i18n.languages.push locale
- debug "loaded #{locale}.json"
- catch e
- debug "failed to load language file #{filePath}"
- else
- debug "path #{dir} doesn't exist"
-
-# Locale validation
-# ------------------
-# Test if a locale is in the format `xx` or `xx-YY`.
-
-i18n.isValidLocale = (locale) ->
- return /^\w\w(-\w\w)?$/.test locale
-
-
-# Accept-Language
-# ----------------
-# Parses an `Accept-Language` header, adds values to the `languages` list and
-# returns the preferred language.
-
-i18n.getLocale = (req) ->
- languages = []
- if acceptHeader = req.header('Accept-Language')
- languages = acceptHeader.split(/,|;/g).filter i18n.isValidLocale
-
- if languages.length < 1
- languages.push i18n.options.default
- debug "Empty Accept-Language header, reverting to default"
-
- for locale in languages when i18n.languages[locale]
- locale = locale.toLowerCase()
-
- # Fallback to default
- locale or= languages[0]
-
- return locale
-
# i18n.plurals
# -------------
# Arguments can be `[n, singular, plural]` or `[n, zero, singular, plural]`.
-# Is invoked by `i18n.translate` when given the correct number ofr arguments.
+# Is invoked by `i18n.translate` when given the correct arguments.
i18n.plural = (str, zero, one, more) ->
if typeof more isnt 'string' then [one, more, zero] = [zero, one, one]
@@ -137,54 +76,52 @@ i18n.plural = (str, zero, one, more) ->
# This is the method used as a template local, usually aliased to '_'.
i18n.translate = (str) ->
- # Handle plurals.
- # Using `apply` and `call` to keep methods in context of this request.
+ # Handle plurals, keep methods in context of this request.
if not isNaN(str) and arguments.length > 2
return i18n.plural.apply @, arguments
- localStrings = (i18n.strings[@locale] or i18n.strings[@lang])
+ if localStrings = i18n.strings[@lang]
+ localStrings[str] ?= '' # Add string to translations if missing
+ return localStrings[str] or str
+ else
+ return str or ''
+
- # Add string to translations if missing
- localStrings && localStrings[str] ?= ''
+# ### .setLanguage()
+# Change language setting for the current user.
- # Get translation
- return localStrings?[str] or str or ''
+i18n.setLanguage = (session, locale) ->
+ lang = locale.split('-')[0]
+ session?.lang = switch true
+ when locale in i18n.languages then locale
+ when lang in i18n.languages then lang
+ else i18n.options.default
-# i18n.setLanguage
-# -----------------
-# Change language setting for the current user.
+ _.debug "Language set to #{session?.lang}"
-i18n.setLanguage = (session, lang) ->
- if lang in i18n.languages
- session.lang = lang
- session.langbase = lang.substring(0,2)
- debug "Language set to #{lang}"
+# ### .loadLanguageFiles()
+# Load language definitions on startup.
-# i18n.updateStrings
-# -------------------
-# Save new strings to language files, use in development mode.
+i18n.loadLanguageFiles = ->
+ @store.load (err, strings) =>
+ _.extend @strings, strings
-i18n.updateStrings = (req, res, next) ->
- basePath = path.join process.cwd(), i18n.options.path
-
- for lang, strings of i18n.strings when i18n.isValidLocale lang
- file = "#{lang}.json"
- filePath = path.join(basePath, file)
- # Re-load file and merge in-memory strings
- fs.readFile filePath, (err, res) ->
- try contents = JSON.parse res.toString()
- catch e then contents = {}
- finally
- # Update in-memory translations or add new strings
- for s, t of strings
- if contents[s] then i18n.strings[lang][s] = contents[s]
- else contents[s] = t
-
- fs.writeFile filePath, JSON.stringify(contents, null, 4), 'utf8'
- debug "Updated strings in #{file}"
+# ### .getLocale()
+# Returns the preferred accepted language for which a translation exists.
+
+i18n.getLocale = (req) ->
+ locale = lang for lang in req.acceptedLanguages when i18n.languages[locale]?
+ return (locale or i18n.options.default).toLowerCase()
+
+# ### .updateStrings()
+# Use in development mode.
+# Refreshes string files and adds new strings on every page load.
+
+i18n.updateStrings = (req, res, next) ->
+ i18n.store.update(i18n.strings)
next()
View
59 src/store.coffee
@@ -0,0 +1,59 @@
+path = require 'path'
+fs = require 'fs'
+_ = require './util'
+
+require 'colors'
+
+# JSON storage
+# -------------
+
+class JSONStore
+
+ constructor: (i18n) ->
+ return new JSONStore unless this instanceof JSONStore
+ @path = path.join process.cwd(), i18n.options.path
+ @default = i18n.options.default
+ @languages = i18n.languages
+
+ # Update strings with translations loaded from storage.
+ update: (strings) ->
+ @load (err, stored = {}) =>
+ _.extend strings, stored
+ @save strings, (err, language) ->
+ _.debug "Updated #{language} strings".blue
+
+ # Save updated strings to disk (asynchronously).
+ save: (strings, callback) ->
+ for language, data of strings
+ do (language) =>
+ file = path.join @path, "#{language}.json"
+ fs.writeFile file, JSON.stringify(data, null, 4), 'utf8', (err) -> callback err, language
+
+ # Load language strings.
+ # When updating strings, this could be simplified a lot if we loaded only the current
+ # user language, but it facilitates testing - a single reload updates all language definitions.
+ load: (callback) ->
+ strings = {}
+ if not fs.existsSync @path
+ _.debug "Path '#{@path}' doesn't exist".red
+ return
+
+ files = fs.readdirSync(@path)
+ .map((f) -> path.basename f, '.json')
+ .filter _.isValidLocale
+
+ for locale in files when locale isnt @default
+ @languages.push locale unless locale in @languages
+ file = path.join @path, "#{locale}.json"
+ try
+ strings[locale] = JSON.parse fs.readFileSync(file).toString()
+ _.debug "Loaded #{locale}.json".blue
+ catch e
+ strings[locale] = {}
+ _.debug "Failed to load file #{locale}.json".red
+
+ callback null, strings
+
+
+module.exports =
+ json: JSONStore
View
13 src/util.coffee
@@ -1,10 +1,13 @@
debugMode = off
-module.exports =
-
+util =
extend: (obj, sources...) ->
for source in sources
- obj[key] = val for key, val of source
+ for key, val of source
+ if typeof val is 'object' and typeof obj[key] is 'object'
+ util.extend obj[key], val
+ else
+ obj[key] = val
return obj
parseJSON: (obj) ->
@@ -20,4 +23,6 @@ module.exports =
debugMode && console.log "[i18n] #{str}"
toggleDebug: (status) ->
- debugMode = status
+ debugMode = status
+
+module.exports = util
View
9 test/i18n.spec.coffee
@@ -1,15 +1,6 @@
assert = require 'assert'
polyglot = require '../'
-describe 'Parse headers', ->
-
- it 'should only accept valid locales', ->
- assert.equal yes, polyglot.isValidLocale value for value in ['pt', 'pt-BR', 'en', 'en-US']
- assert.equal no, polyglot.isValidLocale value for value in ['pt-b', 'pt-', 'p', 'portugues']
-
- it 'should fail silently for non-strings', ->
- assert.equal no, polyglot.isValidLocale value for value in [null, undefined, 123, {}, []]
-
describe 'Translations', ->
polyglot.strings.pt =
Please sign in to comment.
Something went wrong with that request. Please try again.