MongoDB-based Backend::Store implementation for I18n gem

2010 / 12 / 28

Currently I’m working on a web project on top of rails 3 and mongodb.

Recently we faced the necessity of changing I18n backend from simple to something more dynamic (in fact, we wanted to allow the non-geeks from our team to participate in the internationalization process via web interface).

I googled for “mongodb i18n”, “mongo_mapper i18n”, “mongodb i18n backend”, etc. and found nothing, so I wrote my own implementation of mongodb backend for I18n, called mongo-i18n (in fact, «wrote» is too strong a word — all that I needed to do was to implement three methods of the store interface:)

You can grab the sources here or you can install the gem via command line:

gem install mongo-i18n

Usage:

I recommend to start with Chain backend while mongodb collection is empty, and move to entirely mongodb-based backend when you populate your database with all I18n messages.

Begin with:

collection = Mongo::Connection.new['my_app_related_db'].collection('i18n')
I18n.backend = I18n::Backend::Chain.new(I18n::Backend::KeyValue.new(MongoI18n::Store.new(collection)), I18n.backend)

And finish with:

collection = Mongo::Connection.new['my_app_related_db'].collection('i18n')
I18n.backend = I18n::Backend::KeyValue.new(MongoI18n::Store.new(collection)

If you are already using a mongodb ORM in your project (and I suppose you are, why else would you be reading this article? :), I recommend using the existing database connection.

collection = MongoMapper.database.collection('i18n')
I18n.backend = I18n::Backend::KeyValue.new(MongoI18n::Store.new(collection))

Free Christmas gift, the rake file to import en.yml into mongodb:

# usage:
# bundle exec rake locale:file RAILS_ENV=production
# if you want to export a different locale (not en.yml), provide locale option, as follows:
# bundle exec rake locale:file RAILS_ENV=production locale=ru

require 'mongo-i18n'

def write_to_database(sc, path, value)
  key = path.join('.')
  sc[key] = value.to_json
end

# traverse through hash
def traverse(sc, obj, path)
  case obj
  when Hash
    obj.each do |k,v| 
      traverse(sc, v, path + [k]) 
    end
  when Array
    obj.each {|v| traverse(sc, v) }
  else # end values
    write_to_database(sc, path, obj)
  end
end

namespace :locale do
  desc <<-eos
    Exports $app/config/locale/$locale.yml contents to mongodb database. 
    If locale is not specified, default (en) locale file will be exported.
  eos
  task :file do
    locale = ENV['locale'] || "en"
    environment = ENV['RAILS_ENV']   || "development"
    # I keep mongodb connection descriptor in config/mongodb.yml
    config = YAML::load(File.read(Rails.root.join('config/mongodb.yml')))
    collection = Mongo::Connection.new[config["development"]["database"]].collection('i18n')
    store = MongoI18n::Store.new(collection)
    
    dump = YAML::load(File.open(Rails.root.join("config","locales", "#{locale}.yml")))

    traverse(store, dump, [])
  end
end

Any feedback would be welcome.

Quick tip for Intellij Idea users: manage your dependencies order

2010 / 12 / 01

Recently I was developing project on top of Servlet API 3.0 and GWT 2.1 and faced the following problem: When I wanted to use async support from Servlet 3.0, Idea was unable to resolve method startAsync for HttpServletRequest.

Like this:

Resolving method failure

It turns out that gwt-dev-2.1.0.jar contains Servlet API 2.4 (looks like it comes with jetty or tomcat gwt dependencies) and Idea uses it for inspections and autocomplete.

Solution is simple: Check if servlet api 3.0 jar comes after gwt-dev-2.1.0.jar in yout project module dependencies (meta+; shortcut), like this:

Wrong dependency order

And swap them using “Move Up”|”Move Down” keys, than hit OK button. After that Idea will resolve Servlet API as expected:

Resolving method success

Strip firebug console api calls

2009 / 10 / 05

Update: После обсуждения на хабре и продолжительных размышлений на тему, решил отказаться от подобного метода. Думаю пересесть на Google Closure Compiler и с его помощью обрезать debug-код, как расписано в статье у Ильи Кантора. Так что этот пост подустарел и оставляю я его чисто для истории.

На днях надо было сходить показать представителю заказчика, как пользоваться одной свежевстроенной в проект фичей. За полчаса до выхода подготовил новый билд, протестировал.

По приезду оказалось, что у заказчика фича не работает. Неприятно получилось, в общем.

Но радость ваша, мои дорогие читатели, была бы не полной, если бы я не сообщил вам, что там случилось и что я по этому поводу предпринял.

Оказалось, что я забыл удалить в функции-обработчике события вызов firebug’овской консоли (console.log… и т.п.) Со мной вообще часто бывает такое, что я то какой-то символ, случайно ткнув на клавиатуру, допишу, то, наоборот, удалю - короче, использование редакторов без подсветки синтакса и (желательно) анализа структуры кода мне противопоказаны.

Так как я использую apache ant для развертывания приложения на боевой сервер, я дописал маленький скриптик, который уберет все вызовы console.log(/* something */) или console.dir(/* something */) из вашего кода.

Собственно, вот он:

<?xml version="1.0" encoding="UTF-8"?>
<project name="deploy" default="stripFirebugConsoleCalls" basedir=".">
    <!-- место, где сложены наши еще не сжатые и не слитые в один js-скрипты -->
    <property name="js" value="js/"/>

    <!-- регулярка для отлова нездоровых элементов (беззастенчиво утянута с yui builder'a, и слегка доведена напильником)
        http://github.com/yui/builder/blob/master/componentbuild/shared/properties.xml 79-я строка -->
    <property name="firebug.console.regex" value="^.*?(?:console.log|console.dir).*?(?:;|\).*;|(?:\r?\n.*?)*?\).*;).*;?.*?\r?\n" />
    <property name="firebug.console.regex.flags" value="mg" />
    <property name="firebug.console.regex.byline" value="false" />
    <property name="firebug.console.regex.replace" value="" />

    <!-- Сам таргет тоже без затей "вдохновен" YUI Builder'ом, оригинал тут:
        http://github.com/yui/builder/blob/master/componentbuild/3.x/module.xml 19-я строка -->
    <target name="stripFirebugConsoleCalls" description="Replace firebug console calls">
        <replaceregexp byline="${firebug.console.regex.byline}"
                       match="${firebug.console.regex}"
                       replace="${firebug.console.regex.replace}"
                       flags="${firebug.console.regex.flags}">
            <fileset dir="${js}" includes="*.js" />
        </replaceregexp>
    </target>
</project>

Использовать так: создать xml-файл с удобным вам именем (например, boom.xml) и скопипастить в него этот код. Разумеется, стоит поправить значение переменной js, которая указывает на папку с еще не сжатыми js-скриптами. После этого запускаем адскую машину такой командой (для *nix):

ant stripFirebugConsoleCalls -buildfile /path/to/boom.xml

Вот и все. Засим я откланиваюсь до следующего ЧП или конца проекта.