tag:svenfuchs.com,2011:atom Sven Fuchs 2011-02-11T00:00:00Z tag:svenfuchs.com,2011:organizing-translations-with-i18n-cascade-and-i18n-missingtranslations Organizing translations with I18n::Cascade and I18n::MissingTranslations <p>When it comes to I18n one of the questions I get asked most often is how to organize translation keys. So I thought I&#8217;d write down how we&#8217;re doing it at work, and thus <a href="http://github.com/svenfuchs/adva-cms2">adva-cms2</a>.</p> <h2><span class="caps">DRY</span> does not apply to L10n</h2> <p>Before I get into this let me (once again) explain why <span class="caps">DRY</span> doesn&#8217;t apply to L10n though because that&#8217;s the reason why we allow for those deeply nested keys and namespaces in I18n.</p> <p>Here&#8217;s a slide from my <a href="http://vimeo.com/12665914">Anatomy of Ruby I18n</a> talk at Euruko 2010:</p> <pre> class Internationalization &lt; Abstraction def perform @developer.work! end end class Localization &lt; Concretion def perform @translator.work! end end </pre> <p>Internationalization refers to the work we do as developers. E.g. we extract strings from our code and make them available in translation files. Obviously the principle of <span class="caps">DRY</span> applies to this work in the sense that we don&#8217;t want to reimplement those portions of our code that actually looks up translations and stuff like that.</p> <p>But <span class="caps">DRY</span> does <strong>not</strong> apply to Localization and therefor also doesn&#8217;t apply to our translation keys which can be seen as our interface to or contract with our translators. Instead, as developers our job is to pass control and enable translators to define different translations for the (seemingly) same key in different contexts.</p> <p>For example, even if the string &#8220;edit&#8221; works as a translation in almost any context in English, that might not be true for other languages that have richer semantics and might have different translations for &#8220;editing&#8221; different things. As a developer we won&#8217;t ever be able to know these things in advance and so we just can&#8217;t predict which keys can be joined (&#8220;DRYed up&#8221;) and which can&#8217;t. <code>:'post.edit'</code> may or may not have the same translation in every target language as <code>:'user.edit'</code>.</p> <h2>Using I18n::Cascade</h2> <p>That&#8217;s one reason why the I18n <span class="caps">API</span> supports defaults. Using defaults we might express &#8220;either use an existing flash message for this particular model or use the common default message&#8221; like this:</p> <pre> I18n.t(:'flash.post.update.success', :default =&gt; [:'flash.update.success']) </pre> <p>This way translators can decide whether to use the default translation or specify a different one for this particular model. But obviously we don&#8217;t want to type that all the time.</p> <p>Also, Rails&#8217; <code>translate</code> view helper supports a great convention of automatically scoping translation keys to the current controller name and the current view/template name. E.g. the following lines will both look up the same translation key <code>:'posts.show.edit'</code>:</p> <pre> # in posts/show.html.erb t(:'.edit') t(:'posts.show.edit') </pre> <p>Now, why not extend this convention so that the <code>translate</code> view helper always adds defaults so it effectively behaves like this call:</p> <pre> t('posts.show.edit', :defaults =&gt; [:'posts.edit', :edit]) </pre> <p>Enter <code>I18n::Cascade</code>.</p> <p>This <a href="https://github.com/svenfuchs/i18n/blob/master/lib/i18n/backend/cascade.rb">helper module</a> one of the lesser known modules which are shipped with the <a href="http://rubygems.org/gems/i18n">I18n gem</a>. It can be included into compliant I18n backends and can be configured by passing a <code>:cascade</code> option per request.</p> <p>Starting from version 0.5.0 the behaviour above can be achieved like this:</p> <pre> I18n::Backend::Simple.send(:include, I18n::Backend::Cascade) # should really be a module, but i have no idea how/where to include it :/ ActionView::Base.class_eval do def translate(key, options = {}) super(key, options.merge(:cascade =&gt; { :offset =&gt; 2, :skip_root =&gt; false })) end alias t translate end </pre> <p>Now either of these keys could be translated and will be found &#8230; obviously starting with the most specific key <code>:'posts.show.edit'</code> and cascading down to the least specific one <code>:edit</code>:</p> <pre> # en.yml en: edit: Edit posts: edit: Edit show: edit: Edit </pre> <p>I generally recommend using Rails&#8217; controller/view scoping convention. Together with I18n::Cascade it is quite easy to provide some of the more common translations (like &#8220;new&#8221;, &#8220;edit&#8221;, &#8220;delete&#8221;) on common scopes but still allow translators to provide translations for particular view specific keys.</p> <h2>I18n::MissingTranslations</h2> <p>The <a href="http://github.com/svenfuchs/i18n-missing_translations">i18n-missing_translations</a> gem hooks into the <code>I18n::ExceptionHandler</code> class and logs <code>I18n::MissingTranslationData</code> exceptions. It includes:</p> <ul> <li>an in-memory logger that simply holds missing translations during a request or test run and</li> <li>a middleware that can be used to dump the contents of the logger to a file after each request</li> </ul> <p>Pretty handy.</p> <p>Using the in-memory logger makes sense, e.g., in the test environment. E.g. we could add this to the test_helper or Cucumber env:</p> <pre> at_exit { I18n.missing_translations.dump } </pre> <p>This simply outputs a <span class="caps">YAML</span> snippet for all translations that were missing during the test run which can be copied over to the translations file.</p> <p>In development mode one rather will want to log missing translations to an actual translations file. The provided middleware does that by logging to a file <code>missing_translations.yml</code> in your locales dir (which is config/locales if present or the current directory otherwise). You can also pass the filename as an optional argument:</p> <pre> config.app_middleware.use(I18n::MissingTranslations) if Rails.env.development? </pre> <p>The middleware reads and writes per request. That means that on subsequent requests missing translations are added to the <code>missing_translations.yml</code> file. So if you go ahead and copy translations from the <code>missing_translations.yml</code> to your actual locale files you will also want to clear or delete <code>missing_translations.yml</code>.</p> <p>Also note that Rails does not pick up new locale files between requests (I&#8217;d consider that a bug, in development mode it should pick them up). That effectively means that manual changes to a new <code>missing_translations.yml</code> file might be overwritten unless you restart the server. Thus your workflow for finding and moving missing translation keys might look something like this:</p> <ul> <li>start the server</li> <li>click around/work on stuff</li> <li>check config/locales/missing_translations.yml</li> <li>copy any missing translation keys to your actual locale files and correct the translations</li> <li>delete or clear config/locales/missing_translations.yml</li> <li>restart the server</li> </ul> <p>By the way, <a href="https://github.com/rails/rails/blob/master/actionpack/lib/action_view/helpers/translation_helper.rb#L46-54">starting from Rails 3.1</a> the <code>translate</code> view helper will use the <code>:rescue_format</code> facility from I18n 0.5.0 <a href="https://github.com/svenfuchs/i18n/blob/master/lib/i18n/exceptions.rb#L45-48">exception handling</a>. This means that missing translations will be returned as essentially: <code>keys.last.to_s.gsub('_', ' ').titleize</code> wrapped into a span tag with a <code>translation_missing</code> class set. I.e. a missing translation for <code>:'post.show.edit'</code> will return &#8220;Edit&#8221;.</p> <p>Hopefully these hints help working with translations a little bit.</p> <p>Let me know what you think!</p> 2011-02-11T00:00:00Z tag:svenfuchs.com,2011:databaserecorder-recording-and-replaying-the-database-connection DatabaseRecorder - recording and replaying the database connection <p>I&#8217;ve had this idea to <a href="/2010/11/24/why-exactly-don-t-we-stub-that-database">record calls to the database connection</a> for replaying &#8220;real&#8221; results later on a while ago. I&#8217;ve now found a few hours of time on the train and tried it out.</p> <p><strong>tl&#8217;dr</strong>: It doesn&#8217;t seem worth the effort. There&#8217;s no performance gain so far.</p> <p>Here&#8217;s what I found:</p> <h2>Initial implementation</h2> <p>I&#8217;ve started out setting up a simple library <a href="https://github.com/svenfuchs/database_recorder">database_recorder</a> which wraps the <code>execute</code> method on the <code>ActiveRecord::Base.connection</code> into a <code>capture</code> method. The <code>capture</code> method will either record or replay.</p> <ul> <li>If it records it will simply push the result of the current call to <code>connection.execute</code> (i.e. the results from the database) into an array. An at_exit hook will then marshal this array and store it on the disk.</li> <li>If it replays it will (assuming the array has been unmarshalled and loaded before) shift a result set from the array and directly return it &#8211; not calling the database this time (this approach probably won&#8217;t work well with an async database driver).</li> </ul> <p>There&#8217;s a simle convenience method that makes it easy to hook this up in a Cucumber <code>env.rb</code>:</p> <pre> # features/env.rb DatabaseRecorder.record_or_replay!(ENV['REPLAY']) </pre> <p>Now one can control the record vs replay mode by setting the <code>REPLAY</code> env variable like this:</p> <pre> $ cucumber ; record the db connection $ REPLAY=true cucumber ; replay it </pre> <p>After throwing up a first simple implementation I&#8217;ve tried this with the Cucumber feature suite of <a href="http://github.com/svenfuchs/adva-cms2">adva-cms2</a> which contains 92 scenarios with 869 steps and uses an SQlite3 database for the tests. I was surprised to see that quite some features actually passed already.</p> <h2>Tweaking it</h2> <p>A few more tweaks were necessary though:</p> <p>Recording and replaying <code>execute</code> isn&#8217;t enough, one also has to take care of such methods as <code>last_insert_row_id</code> which sits on the native database connection object which only is accessible as an instance variable. The implementation of the ActiveRecord SQlite3 adapter makes it so that there&#8217;s no single method and no single collection of methods that could be recorded/replayed like this. Instead I had to wrap the <code>execute</code> method on the <code>ActiveRecord::Base.connection</code> and <code>last_insert_row_id</code> and <code>changes</code> on the native SQlite3 database connection object it holds. A little bit ugly, but it works (and once again proves that stuff like <a href="http://en.wikipedia.org/wiki/Law_of_Demeter">LoD</a> is totally valid for Ruby/Rails code as well and one should care about it for widely used libraries).</p> <p>Once I&#8217;ve figured this out almost all scenarios passed. The remaining ones were related to the users signup process.</p> <p>It now occured to me that my initial assumption (of course) wasn&#8217;t accurate: an application won&#8217;t always pass exactly the same data down to the database even with tests always running in the same order and test data not being generated randomly but explicitely defined. There&#8217;s stuff like random tokens! Wow.</p> <p>Ok, so I added a line <code>Devise.stubs(:friendly_token).returns('12345')</code> &#8230; which made all tests pass. Basically the revelation here is that in order to make this record/replay approach work one needs to make absolutely sure that data is passed to the database in a completely deterministic manner. This may or may not an issue depending on the app, testing philosophy/assumptions and other things.</p> <p>Now all 92 scenarios were green!</p> <h2>Results</h2> <p>The <code>db_session</code> file that contains the recorded, marshalled database connection results is ~1.4 MB big &#8230; just saying.</p> <p>When I now looked at the run times of both test suite runs I was shocked to notice that both took almost exactly the same time. In fact the test run which was recording the database connection was slightly faster than the one which replayed it. So far I have no clue about the reasons for that. I would think the code that actually &#8220;replays&#8221; results is simple enough (just doing <code>Array.shift</code>) to outrule the actual database connection. I might overlook something else (garbage collection? &#8230; was running on <span class="caps">MRI</span> Ruby 1.8.7) but so far it seems that recording test runs are consistently (very) slightly faster than replaying ones.</p> <p>How disappointing, this would have been nice.</p> <p>Maybe someone else will pick this experiment up and find something I&#8217;ve missed!</p> <p>Otherwise, I guess, at least I&#8217;ve tried it and proved myself wrong ;)</p> 2011-02-09T00:00:00Z tag:svenfuchs.com,2011:travis-a-distributed-build-server-tool-for-the-ruby-community Travis - a distributed build server tool for the Ruby community <p>So, I&#8217;ve started playing with the idea of a distributed build server tool once again in last year&#8217;s December &#8230; and <a href="https://github.com/svenfuchs/travis">Travis</a> is what I came up with.</p> <h2>Status quo</h2> <ul> <li>Travis is currently running on <a href="http://travis.heroku.com">http://travis.heroku.com</a> and already notifies me about builds that pass or fail when someone pushes to those repositories I&#8217;ve registered for testing purposes.</li> <li>The builds run on a virtual server that <a href="http://twitter.com/railshoster">Julian Fischer</a> has very kindly granted for this experiment.</li> <li>The frontend is implemented using <a href="http://documentcloud.github.com/backbone">Backbone.js</a>.</li> <li>Build output and status information is &#8220;tailed&#8221; from the workers to the frontend (i.e. browsers) using websockets via <a href="http://pusherapp.com">Pusher</a>. It is also pushed back to application (running on Heroku) using a standard <span class="caps">REST</span> <span class="caps">API</span>.</li> </ul> <p>All of these building blocks might change in the future, but here&#8217;s an <a href="http://github.com/svenfuchs/travis/raw/master/docs/travis.spike-2.png">overview</a> of how they currently work together. And here&#8217;s a short screencast (the UI has changed a bit in the meanwhile but you&#8217;ll get the idea): <a href="http://www.youtube.com/watch?v=mNOwCJhjWAw" title="spike 2">1:20 quick demo screencast</a></p> <h2>The vision</h2> <p>Maybe <a href="https://github.com/jenkinsci/jenkins">Jenkins CI</a> (formerly Hudson) is the best open-source build server tool for Ruby projects out there today and everybody seems to be using it. Or maybe that&#8217;s just my impression. In any case my feeling is that it just isn&#8217;t a particularly good fit to the Ruby community for a couple reasons.</p> <p>Instead, imagine a simple and slim build server tool that is maintained by the Ruby community itself (just like Gemcutter is, or many other infrastructure/tool-level projects are) in order to support all the open-source Ruby projects/gems we&#8217;re using every day.</p> <p>Why not have a very slick application that loads off all the heavy work to workers &#8211; and have workers started on boxes contributed by the community itself. A lot of people have underused boxes idling at their offices or somewhere else. Why not make it easy for them to contribute run-time on these boxes for building open-source Ruby projects they use every day. I am sure many people would love this kind of opportunity of giving something back. On top of that one could even post a tiny message alongside with builds that ran on their boxes (a la &#8220;this build was kindly sponsored by [link to their website]&#8221;) to make this even more appealing.</p> <p>A system that uses a client-side JS frontend, websocket messages and a tiny <span class="caps">JSON</span> <span class="caps">API</span> will make it easy for people to come up with alternate UI implementations. The current implementation of Travis should already allow building a custom dashboard or mobile app pretty easily.</p> <p>I have no idea whether this will actually happen. But the vision of building an open-source build server infrastructure that is as much community-driven as Gemcutter is, allowing the community to contribute the required computing resources in order to build projects they love and use &#8230; really fascinates me.</p> <h2>Please join and help</h2> <p>My first goal with this experiment was to build a simple system that already makes sense as a stand-alone, privately maintained tool for running private builds in a closed environment and as such it seems to work quite fine so far.</p> <p>Now I think the next step should be to get you involved :)</p> <p>Please get on board and help pushing this project. E.g.:</p> <ul> <li>Register to the <a href="http://groups.google.com/group/travis-app">Google group</a> and share your opinion</li> <li>Pop in to the <span class="caps">IRC</span> channel <a href="irc://irc.freenode.net#travis">irc://irc.freenode.net#travis</a> and say hi</li> <li>Try out the application on <a href="http://travis.heroku.com">http://travis.heroku.com</a>, sign in and set up a service hook for some of your open Github repositories.</li> <li>Review <a href="https://github.com/svenfuchs/travis">the code</a> and tell how to improve, e.g., the client-side JS application, the EM-based worker code, the architecture in general etc.</li> <li>Have a look at <a href="https://github.com/svenfuchs/travis/issues">tickets filed on Github</a>, discuss and help resolving them.</li> <li>Share ideas about how to improve the UI design (I think the current minimalistic design works for now, but certainly isn&#8217;t good. I don&#8217;t consider myself a design person, so any input is highly appreciated)</li> <li>Come up with ideas about how to implement a dynamic spin-up of workers on EC2 instances, VMs running workers securely on contributed boxes.</li> <li>Build an alternate websocket service in case we can&#8217;t use Pusher.app in future (Travis currently uses a free dev account on Pusher.app and paid plans seem quite expensive).</li> <li>Whatever else you&#8217;re interested in &#8230; :)</li> </ul> <p>If you have any questions just <a href="http://groups.google.com/group/travis-app">post to the mailing list</a> or drop me a note via <a href="http://twitter.com/svenfuchs">Twitter</a>, <a href="http://github.com/svenfuchs">Github</a> or <a href="mailto://[email protected]">email</a>.</p> <p>Some of the questions that I definitely need some input for are:</p> <ul> <li>This is the first single-page Backbone.js app I&#8217;ve done. Does my interpretation of JS <span class="caps">MVC</span> make sense? Should I change the way views are instantiated and/or bound to events?</li> <li>How to dynamically spin up workers in VMs on EC2 instances or other boxes?</li> <li>How to improve the websocket message flow from workers via Pusher to the browser? E.g. sometimes messages arrive at the browser in the wrong order because they&#8217;re not synchronized or ordered in any way.</li> </ul> <p>In any case your input is highly appreciated!</p> 2011-02-05T00:00:00Z tag:svenfuchs.com,2011:why-exactly-don-t-we-stub-that-database Why exactly don't we stub that database? <p>Ok, when it came to unit tests I&#8217;ve been in the heavy-mocking-and-stubbing camp a few years ago. But then I&#8217;ve then seen our team (including myself) crash into the same issue over and over again: the fact that with mocks/stubs one does not test the real thing. One tests mocks and stubs which can easily get out of sync with the &#8220;real thing&#8221;, i.e. ActiveRecord models in our case.</p> <p>So I&#8217;ve completely turned away from my previous position. Our tests became somewhat slower, hitting the database all the time, but they also were more robust, easier to read and easier to change. We could be pretty sure that our tests test the real application and things that were broken would actually be caught by the test suite.</p> <p>Now while I&#8217;ve been watching Mike Perham&#8217;s talk about <a href="http://vimeo.com/10849958">Scalable Ruby Processing with EventMachine</a> something rang a bell for me. At some point Mike mentions that in order to write a non-blocking Postgres adapter he only needed to change a single method: <code>execute</code> on the Postgres adapter. For some reason the question popped into my mind if that couldn&#8217;t apply to mocks/stubs for unit tests as well.</p> <p>Actually when we say that we &#8220;mock out the database&#8221; that&#8217;s not even close to what we&#8217;re really doing. In fact we are using an arbitrary object that pretends to behave the same way as the &#8220;real&#8221; one. That&#8217;s something completely different from really mocking the database if you think about it &#8211; and to me it seems this difference is the same difference that I&#8217;ve been experiencing when mocks became out of sync with the actual application: some logic in the model had changed but an according change to the mocks was missing. Because my &#8220;mocks&#8221; didn&#8217;t actually mock the database but the whole model.</p> <p>So what if we&#8217;d use a record-replay approach instead that wraps around the adapter&#8217;s <code>execute</code> method and records responses from the database when our tests run for the first time. On subsequent requests it would not hit the database any more but actually mock it by replaying the results from the first run.</p> <p>[Update:] <a href="https://github.com/myronmarston/vcr"><span class="caps">VCR</span></a> is a test helper library that does something similar for <span class="caps">HTTP</span>. The <span class="caps">README</span> says: &#8220;Record your test suite&#8217;s <span class="caps">HTTP</span> interactions and replay them during future test runs for fast, deterministic, accurate tests.&#8221;</p> <p>Let&#8217;s assume we have this totally made up test. When we run it for the first time the following happens:</p> <pre> User.create!(:name =&gt; 'David') # =&gt; pass the CREATE query to the db and record that it's in state abcd (hash from the query) david = User.find_by_name('David') # =&gt; pass the FIND query to the db and record the result for state abcd david.name = 'Josh' david.save # =&gt; pass the UPDATE query to the db and record that it's in state 1234 (hash from the query) david.reload # =&gt; pass the FIND query to the db and record the result for state 1234 assert_equal 'Josh', david.name # =&gt; GREEN </pre> <p>Running the same test again would allow the <code>execute</code> mock to kick in, preventing the test from hitting the database:</p> <pre> User.create!(:name =&gt; 'David') # =&gt; take a note that the db is in state abdc (hash from the CREATE query) david = User.find_by_name('David') # =&gt; return the result previously recorded for this FIND query executed on state abdc david.name = 'Josh' david.save # =&gt; take a note that the db is in state 1234 (hash from the UPDATE query) david.reload # =&gt; return the result previously recorded for this FIND query executed on state 1234 assert_equal 'Josh', david.name # =&gt; GREEN </pre> <p>Obviously when we now make a change to either the application or test code that changes the interaction with the database then our recorded responses would become stale. In that case a query would get executed that has not been recorded before so we can stop the test suite from executing and ask the developer to re-record the results from the actual database first. We could even consider stopping the test suite, wiping out any results and re-run the test suite in recording mode automatically.</p> <p>This way we would actually stub the database, not the model. Our tests would still go through all the low level ActiveRecord code that turns database result sets into model instances, which will be slower than using fake models but also will be much more robust and make our tests more useful.</p> <p>The other question obviously is, how much run time could we spare this way? Obviously we&#8217;d need some kind of temporary place (maybe Redis?) to store those db states between test runs. So in the end we&#8217;d use one database to stub another one. Hu?</p> <p>Let me know what you think.</p> 2010-11-24T00:00:00Z tag:svenfuchs.com,2011:travis-an-experimental-distributed-ci-server-on-heroku Travis - an experimental, distributed CI server on Heroku <p><strong><em>Please note that this information is completely outdated.</em></strong></p> <p><strong><em>If you've got here via Google searching for a distributed build server tool that runs on Heroku, then please refer to <a href="/2011/2/5/travis-a-distributed-build-server-tool-for-the-ruby-community">this article</a>.</em></strong></p> <p>We had the I18n gem tested on runcoderun, a great service for open source projects to run continous integration on the web, and this was working very well for us. Unfortunately runcoderun was <a href="http://blog.runcoderun.com/post/463439385/saying-goodbye-to-runcoderun">taken down</a>.</p> <p>So I figured it should be easy to setup a few apps on Heroku and have them run the I18n test suite on different <a href="http://docs.heroku.com/stack">stacks</a> (i.e. Ruby configurations on Heroku). It turns out it is not, apparently. Or I'm just too stupid to do it. Anyway I though I'd write things down so maybe someone else can pick this experiment up and push it a few steps farther.</p> <p>The current state is that <a href="http://github.com/svenfuchs/travis">Travis</a> can automatically set up a single ci server and 3 ci runners on Heroku. The server app takes the ping from github and pings the 3 runners. Each of the runners forks and immediately returns from the parent (forking) process while the child process runs the build command and posts the result back to the server. The server stores the result and displays it on the builds index page.</p> <p>Everything's cool except that the runner does not work properly on neither aspen-mri-186 (i.e. Ruby 1.8.6) nor bamboo-mri-191 (i.e. Ruby 1.9.1). It seems to work fine on the bamboo-ree-187 stack though and I will leave it running for a while so we can see how it works out over a few commits to the I18n repository.</p> <h2>Setup</h2> <p>Even though I've published a few versions of Travis as a gem it's probably easier for you to just clone or fork the repository and mess with it directly.</p> <p>Take a look at the files in the <a href="http://github.com/svenfuchs/travis/tree/master/ci/">ci/</a> folder. You can configure which repository you want to test, which stacks you want (possible values are 186, 187, 191) and which command you want to be run.</p> <p>Once you've tweaked these values according to your library you can run</p> <pre>$ ./bin/travis install</pre> <p>This will take a while as it creates four applications on Heroku (if you choose to use all three Heroku stacks), sets up a temporary Git branch containing only the relevant files and pushes this branch to each of the Heroku apps. (You obviously need Heroku to be set up locally for this to work.)</p> <p>The runner uses <a href="http://github.com/integrity/bob">Bob</a> to check out your repository to the tmp/ directory and run the test command that you've set. (Hence the name Travis. I found it's the most suitable cool name from the Bob the Builder series that fits a CI server library.) The server uses Datamapper for storing the build results.</p> <p>You can destroy this setup by running</p> <pre>$ ./bin/travis destroy</pre> <p>The applications will be named like this:</p> <pre>http://ci-[your_repo_name].heroku.com http://ci-[your_repo_name]-runner-[stack].heroku.com</pre> <p>E.g. for the I18n gem this would be:</p> <pre>http://ci-i18n.heroku.com http://ci-i18n-runner-186.heroku.com http://ci-i18n-runner-187.heroku.com http://ci-i18n-runner-191.heroku.com</pre> <h2>Testing</h2> <p>You can then go to the admin page of your Github repository and setup a post-receive hook by simply adding the ci server url to the "Post-Receive URLs" section.</p> <p>Then you can test the whole thing by clicking on the "Test Hook" button. This will post a payload containing the latest commit information to your Travis ci server.</p> <p>Afterwards the server app should display the test run on at least the 1.8.7 stack, something like this: <a href="http://ci-i18n.heroku.com/">http://ci-i18n.heroku.com</a></p> <p>You can also take a look at the applications' log files by running</p> <pre>$ heroku logs --app ci-i18n (for the server) $ heroku logs --app ci-i18n-runner-187 (for the 1.8.7 runner)</pre> <h2>Problems on 1.8.6 and 1.9.1</h2> <p>I have no idea why this setup fails on aspen-mri-186 and bamboo-mri-191. Both work fine when I run the whole thing locally using Thin as a server and RVM for switching Ruby stacks.</p> <p>But for some reason on the bamboo-mri-191 stack the only log output I get on the runner is the output from Bob (about fetching the master branch from the repository etc.) while on aspen-mri-186 there seems to be a segfault happening in eventmachine.</p> <p>Most probably both failures have something to do with the fakt that the Travis runner process forks itself in order to immediately return from the POST request from the server then continue running the test suite in the child process. Maybe I'm doing it wrong? Or maybe it helps to use a Thread for this purpose?</p> <p>The reasoning behind this asynchronous setup is that a synchronous setup would not work when any of the test suite builds takes longer than 30 seconds because Heroku seems to reap processes running longer than that. I've also already had trouble with the I18n test suite which takes ~ 10 seconds because these add up to > 30 seconds on three runners.</p> <h2>Help needed</h2> <p>I probably won't have the time to investigate this much further soon ... and I feel I might not be the right person either because I have no idea what might be going on on Heroku with the process forking. </p> <p>Maybe someone with a stronger knowledge about low-level unixy things and the differences in various Ruby versions could help.</p> <p>Anyway I'm putting this here for you to pick up. If we could get this thing working on at least the 1.9.1 stack this could be a great contribution to the Ruby community because currently there's no easy or cheap way to set up continous integration for open source projects on various Ruby stacks.</p> <p>Feel free to contact me for any questions :)</p> 2010-06-16T00:00:00Z tag:svenfuchs.com,2011:release-your-gems-with-ease Release your Gems with ease <p>After the <a href="http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended">recent, somewhat heated discussion</a> about whether or not to check in gemspecs to a repository, manually crafting versus autogenerating them, using a dynamic piece of code to collect relevant files versus maintaining a static files list in the gemspec file ... I've felt motivated to polish my own <a href="http://github.com/svenfuchs/gem-release">gem plugin</a> that I've been using to publish my gems and finally add some of the functionality that I've been missing myself.</p> <p>In short, I'm a happy resident in the camp of manually maintained gemspecs - as long as I have a dynamic snippet of Ruby code collecting my files for me. Just check in a well done gemspec and you're good to go. (If you're interested in more explanation then see below.)</p> <p>So, here's my rubygems <a href="http://github.com/svenfuchs/gem-release">gem-release</a> plugin.</p> <p>It's pretty simple, could as easily be done in Rake (I don't like Rake though) or Thor and maybe only suits my own needs well. In fact, if you don't publish any gems that often or aren't familiar with the process I'd probably recommend <a href="http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended">doing all of this manually</a>. Also, obviously there are other, mostly Rake based, tools like <a href="http://github.com/technicalpickles/jeweler">Jeweler</a>, <a href="http://github.com/mojombo/rakegem">rakegem</a> or even <a href="http://github.com/seattlerb/hoe">Hoe</a>. And if your happy with Textmate snippets, then Mislav <a href="http://gist.github.com/356455" title="gist: 356455 - TextMate snippet to quickly populate a fresh gemspec- GitHub">has one</a> for you that generates a gemspec.</p> <p>The <a href="http://github.com/svenfuchs/gem-release">gem-release plugin</a> adds four commands to the <code>gem</code> command:</p> <pre class="shell"> $ gem bootstrap # bootstraps a new gem repository $ gem gemspec # generates a gemspec file with sane defaults $ gem tag # creates a git tag and pushes it to the origin repository $ gem release # builds the gem and pushes it to rubygems.org </pre> <p>For more a detailled explanation of these commands and their options see the <a href="http://github.com/svenfuchs/gem-release#readme">README</a>.</p> <p>The <code>bootstrap</code> command can optionally invoke the gemspec command. And the release command can optionally invoke the tag command. So this is what my workflow looks like:</p> <pre class="shell"> $ mkdir foo-bar; cd foo-bar $ gem bootstrap --scaffold --github # code away # check the generated gemspec file and add/edit stuff $ git add/commit/push $ gem release --tag # come back later ... # code away, manually bump version in lib/foo_bar/version.rb $ git add/commit/push $ gem release --tag </pre> <p>The tool assumes that you're following the good practice of having a <code>lib/[name]/version.rb</code> that defines <code>[Name]::VERSION</code> (and it will add that file for you if you use the <code>--scaffold</code> option on <code>gem bootstrap</code>).</p> <p>It also assumes that you want a git-based strategy for dynamically collecting files in your gemspec (i.e. <code>`git ls-files {app,lib}`.split("\n")</code>). If you pass the option <code>--strategy glob</code> then you get a glob instead (i.e. <code>Dir['{lib/**/*,[A-Z]*}']</code>). Obviously you're free to change that in any way you want (and I'll happily accept pull requests for different strategies).</p> <p>Also, the tool currently assumes that you have a bunch of config variables defined in your global Git setup. I.e. it will populate:</p> <pre class="shell"> authors from `git config --get user.name` email from `git config --get user.email` homepage from `git config --get github.user` (http://github.com/[github.user]/[gem_name]) </pre> <p>And obviously it uses <code>`git config --get github.user`</code> and <code>`git config --get github.token`</code> to create a repository on Github when you pass the <code>--github</code> option to <code>gem bootstrap</code>.</p> <p>Enjoy :)</p> <hr /> <p>PS:</p> <p>In case your interested in this discussion at all here's my point of view.</p> <p>I don't want to manually maintain a list of files. I'm a slacker, so that's just to cumbersome and errorprone to me. But I don't really want to use an external tool for maintenance (like <a href="http://github.com/technicalpickles/jeweler">Jeweler</a>, which I've been using a lot before) as I tend to forget updating the gemspec before I push.</p> <p>Also, given the assumption that it is ok to have dynamic code in the gemspec I don't think it makes a lot of sense to keep gem meta information in a Rake file instead of the gemspec (like authors, email, homepage ...). And I'm definitely not concerned with any left over swap files, <code>.svn</code> directories or other rubbish making their way into my gems because I just don't have them in the relevant directories.</p> <p>So this is one of the rare cases where I think code generation is a good thing. I'll just have a tool to generate my gemspec once and then I can manually maintain it in case I have to add a dependency or something.</p> <p>I can't see why I shouldn't be checking the file in to my repository this way either. It's not autogenerated in the sense that I'd use some external tool to rebuild it from a git pre-commit hook or something. Instead it's scaffolded just as any Rails app is.</p> <p>One thing I find nice about this approach is that my stuff works without any dependencies. People who check out the repository will be able to just do <code>gem build; gem install</code> and they're done. The fact that I'm using a dynamic piece of code to collect the files means that I don't need to worry about the file being in sync with recent changes to the codebase.</p> 2010-04-05T00:00:00Z tag:svenfuchs.com,2011:aligning-rubygems-bundler-through-rubylib-environment-variable Aligning rubygems + bundler through rubylib environment variable <p>Put this into your ~/.zprofile (or ~/.profile or whatever makes sense for your shell):</p> <pre class="shell"> export RUBYLIB=$RUBYLIB:~/.ruby </pre> <p>Source the file:</p> <pre class="shell"> $ source ~/.zprofile </pre> <p>Put this to ~/.ruby/b.rb</p> <pre class="ruby"> begin # should probably check parent directories, too? require File.expand_path('../.bundle/environment') rescue LoadError require 'rubygems' require 'bundler' Bundler.setup end </pre> <p>Done.</p> <p>You can now:</p> <pre class="shell"> $ ruby -rb test/all.rb # run test/all.rb in the context of your bundle $ ruby -rubygems test/all.rb # run test/all.rb in the context of rubygems </pre> 2010-03-31T00:00:00Z tag:svenfuchs.com,2011:experimental-ruby-i18n-extensions-pluralization-fallbacks-gettext-cache-and-chained-backend Experimental Ruby I18n extensions: Pluralization, Fallbacks, Gettext, Cache and Chained backend <h2>Backend::Simple &lt; Backend::Base</h2> In Ruby what is the most obvious, elegant and maintainable pattern to extend an existing class' or object's functionality? No, the answer to that is definitely *not* in using <code>alias_method_chain</code>. It's simply including a module to that class. You probably knew that already ;) We've done this with the I18n <code>Simple</code> backend before but one needs to extend the <code>Simple</code> backend first in order to then inject additional modules to the inheritance chain so that these modules' methods would be able to call super and find the original implementation. To make this a bit easier I've moved the original <code>Simple</code> backend implementation to a new <code>Base</code> backend class and simply extend the (otherwise empty) <code>Simple</code> backend class from it (<a href="http://github.com/svenfuchs/i18n/blob/fa6a468f28cdd96c83c8c696591e3040af4efec8/lib/i18n/backend/base.rb">see here</a> and <a href="http://github.com/svenfuchs/i18n/blob/fa6a468f28cdd96c83c8c696591e3040af4efec8/lib/i18n/backend/simple.rb">here</a>). This way you now do not need to extend the <code>Simple</code> backend class yourself but you can directly include your modules into it: <pre><code class="ruby">module I18n::Backend::Transformers def translate(*args) transform(super) end def transform(entry) # your transformer's logic end end I18n::Backend::Simple.send(:include, I18n::Backend::Transformers)</code></pre> I have no clue what your <code>Transformers</code> module could do exactly but that's the point about extensible libraries, isn't it? In any case this is simply the pattern that the new, experimental <code>Pluralization</code>, <code>Fallbacks</code>, <code>Gettext</code> and <code>Cache</code> modules use that I wanted to talk about :) <h2>Pluralization</h2> Out-of-the-box pluralization for locales other than <code>:en</code> has been recurring requests for the I18n gem. Even though it was easy to extend the <code>Simple</code> backend to plug in custom pluralization logic and there are working backends doing that (e.g. in <a href="http://github.com/joshmh/globalize2">Globalize2</a>) there does not seem to be a point in still rejecting a basic feature like this from being included to I18n. I've thus added a <code>Pluralization</code> module that was largely inspired by <a href="http://github.com/yaroslav/i18n/tree/lambda">Yaroslav's work</a>. It can be included to the <code>Simple</code> backend (or other compatible backend implementations) and will do the following things: * overwrite the existing <code>pluralize</code> method * try to find a pluralizer shipped with your translation data and if so use it * call super otherwise and use the default behaviour One can ship pluralizers (i.e. lambdas that implement locale specific pluralization algorithms) as part of any Ruby translation file anywhere in the <code>I18n.load_path</code>. The implementation expects to find them with the key <code>:pluralize</code> in a (newly invented) translation metadata namespace <code>:i18n</code>. E.g. you could store an Farsi (Persian) pluralizer like this: <pre><code class="ruby"># in locales/fa.rb { :fa => { :i18n => { :pluralize => lambda { |n| :other } } } }</code></pre> And include the <code>Pluralization</code> module like this: <pre><code class="ruby">require "i18n/backend/pluralization" I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)</code></pre> We still have to figure out how to actually ship a bunch of default pluralizers best, but you can find a complete list of <a href="http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html" title="Language Plural Rules">CLDR's language plural rules</a> compiled to Ruby <a href="http://github.com/svenfuchs/i18n/blob/e4ce5e58f0524ae7c34ca94971363e13aa889f36/test/fixtures/locales/plurals.rb">here</a> (part of ours test suite). <h2>Locale Fallbacks</h2> Another feature that was requested quite often, too, is Locale fallbacks. <code>Simple</code> backend just returns a "translation missing" error message or raises an exception if you tell it so. It won't check any other locales if it can't find a translation for the current or given locale though. There were proposals for a <a href="https://rails.lighthouseapp.com/projects/8994/tickets/2637-patch-i18n-look-up-a-translation-with-the-default-locale-when-its-missed-with-another-specific-locale">minimal fallback functionality</a> that just checks the default locale's translations if a translation is not available for the current locale. <a href="http://github.com/joshmh/globalize2">Globalize2</a> on the other hand ships with a quite powerful Locale fallbacks implementation that also enforces <a href="http://en.wikipedia.org/wiki/IETF_language_tag" title="IETF language tag">RFC 4646/47 standard compliant locale (language) tags</a>. I've discussed this with Joshua and we've decided to extract a simplified version from <a href="http://github.com/joshmh/globalize2">Globalize2</a>'s fallbacks that makes the <a href="http://tools.ietf.org/html/rfc4646" title="RFC 4646 - Tags for Identifying Languages">RFC 4646</a> standard compliance an optinal feature but still allows enough flexibility to define arbitrary fallback rules if you need them. If you don't define anything it will just use the default locale as a single fallback locale. Again enabling Locale fallbacks is just a matter of including the module to any compatible backend: <pre><code class="ruby">require "i18n/backend/fallbacks" I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)</code></pre> This overwrites the <code>Base</code> backend's <code>translate</code> method so that it will try each locale given by <code>I18n.fallbacks</code> for the given locale. E.g. for the locale <code>:"de-DE"</code> it will try the locales <code>:"de-DE"</code>, <code>:de</code> and <code>:en</code> until it finds a result with the given options. If it does not find any result for any of the locales it will then raise a <code>MissingTranslationData</code> exception as usual. The <code>:default</code> option takes precedence over fallback locales, i.e. it will first evaluate a given default option before falling back to another locale. You can add custom fallback rules to the I18n.fallbacks instance like this: <pre><code class="ruby"># use Spanish translations if Catalan translations are missing: I18n.fallbacks.map(:ca => :"es-ES") I18n.fallbacks[:ca] # => [:ca, :"es-ES", :es, :en]</code></pre> If you do not add any custom fallback rules it will just use the default locale and the default locales fallbacks: <pre><code class="ruby"># using :"en-US" as a default locale: I18n.default_locale = :"en-US" I18n.fallbacks[:ca] # => [:ca, :"en-US", :en]</code></pre> If you want RFC 4646 standard compliance to be enforced for your locales you can use the <a href="http://github.com/svenfuchs/i18n/blob/e7bf15351cd2e27f5972eb40e65a5dd6f4a0feed/lib/i18n/locale/tag/rfc4646.rb">Rfc4646 Tag</a> class: <pre><code class="ruby">I18n::Locale::Tag.implementation = I18n::Locale::Tag::Rfc4646</code></pre> This will make a locale "de-Latn-DE-1996-a-ext-x-phonebk-i-klingon" fall back to the following locales in this order: <pre><code class="ruby">de-Latn-DE-1996-a-ext-x-phonebk-i-klingon de-Latn-DE-1996-a-ext-x-phonebk de-Latn-DE-1996-a-ext de-Latn-DE-1996 de-Latn-DE de-Latn de</code></pre> Most of the time you probably won't need anything like this. Thus we've used the (much cheaper) <code>I18n::Locale::Tag::Simple</code> class as the default implementation. It simply splits locales at dashes and thus can do fallbacks like this: <pre><code class="ruby">de-Latn-DE-1996 de-Latn-DE de-Latn de</code></pre> Should be good enough in most cases, right :) <h2>Gettext</h2> The difference between the Gettext support and all the other extensions discussed here is that I haven't had a real use for it myself - so far, so please consider this stuff highly experimental. It shares the fact that people requested it <a href="http://groups.google.com/group/rubyonrails-core/browse_thread/thread/e9e219ff318fa6e7/8952b9da6b107b50">in one way or the other</a> though, so I'd appreciate feedback about it. Gettext support comes with three parts, only two of them being relevant to the user: * <a href="http://github.com/svenfuchs/i18n/blob/fa6a468f28cdd96c83c8c696591e3040af4efec8/lib/i18n/helpers/gettext.rb">classical Gettext-style accessor helpers</a> * <a href="http://github.com/svenfuchs/i18n/blob/fa6a468f28cdd96c83c8c696591e3040af4efec8/lib/i18n/backend/gettext.rb">a Gettext PO file compatible backend storage (currently read-only)</a> * <a href="http://github.com/svenfuchs/i18n/blob/fa6a468f28cdd96c83c8c696591e3040af4efec8/lib/i18n/gettext.rb">a few internal helpers</a> To include the accessor helpers to your application you can simply include the module whereever you need them (e.g. in your views): <pre><code class="ruby">require "i18n/helpers/gettext" include I18n::Helpers::Gettext</code></pre> The backend extension is, again, a matter of including the module to a compatible backend (e.g. <code>Simple</code>): <pre><code class="ruby">require "i18n/backend/gettext" I18n::Backend::Simple.send(:include, I18n::Backend::Gettext)</code></pre> Now you should be able to include your Gettext translation (\*.po) files to the I18n.load_path so they're loaded to the backend and you can use them as usual: <pre><code class="ruby">I18n.load_path += Dir["path/to/locales/*.po"]</code></pre> Please note that following the Gettext convention this implementation expects that your <em>translation files are named by their locales</em>. E.g. the file <code>en.po</code> would contain the translations for the English locale. <h2>Translate Cache</h2> Right from the beginning people <a href="http://groups.google.com/group/rails-i18n/browse_thread/thread/e16079ef9b24d9/d6dbeb958555edbf" title="Quick benchmarks (dramatic results)">benchmarked the I18n gem implementation</a> and compared their numbers against native Hash lookups or other, less rich implementations of <a href="http://github.com/grosser/fast_gettext/blob/2ea5ccb486b1cfd4bacb0c0748db713765b23b9f/README.markdown">similar APIs</a>. Also, <a href="http://github.com/thedarkone">thedarkone</a> has experimented with a <a href="http://github.com/thedarkone/i18n">Fast backend</a> that implements some significant optimizations. For the I18n gem itself we've refrained from overly extensive "early optimizations" and only applied some tweeks that made the implementation cheaper in obvious ways. This rather conservative approach actually paid out: the clean and readable implementation made the <code>Simple</code> backend refactorings and extensions (like those discussed here) almost trivial. On the other hand, even though calls to I18n don't actually add <em>that</em> much load to an application sparing resources obviously is an important concern. The probably both most effective and least intrusive way of doing that is simply caching all calls to the backend. Again you can include the Cache layer by simply including the module: <pre><code class="ruby">require "i18n/backend/cache" I18n::Backend::Simple.send(:include, I18n::Backend::Cache)</code></pre> As we do not provide any particular cache implementation though you also have to set your cache to the I18n backend. The cache layer assumes that you use a cache implementation compatible to <a href="http://api.rubyonrails.org/classes/ActiveSupport/Cache.html">ActiveSupport::Cache</a>. If you're using this in the context of Rails this is a matter of one line: <pre><code class="ruby">I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)</code></pre> Obviously this pluggable approach again allows you to pick whatever cache is most appropriate for your setup. For example <a href="http://api.rubyonrails.org/classes/ActiveSupport/Cache.html">ActiveSupport</a> out of the box ships with compatible implementations for plain memory, drb demon and compressed, synchronized and plain memcached storage - wow. These options should be sufficient for 99% of all Rails apps. Whatever cache implementation you use the I18n backend cache layer will simply cache results from calls to <code>translate</code> (and will do the right thing with MissingTranslationData exceptions raised in your backend). For that it relies on the assumption that calls to the backend are <a href="http://en.wikipedia.org/wiki/Idempotence" title="Idempotence">idempotent</a>: <em>"A unary operation is called idempotent if, whenever it is applied twice to any value, it gives the same result as if it were applied once."</em>. I18n's library design does not garantuee that by itself but in all practical cases will behave this way unless your doing really weird things with it. Basically, make sure you only pass objects to the translate method that respond to the <a href="http://www.ruby-doc.org/core/classes/Object.html#M000337">hash</a> method correctly. If you use custom lambda translation data make sure they always return the same values when passed the same arguments. <h2>Chained backend</h2> The <code>Chain</code> backend is another feature ported from <a href="http://github.com/joshmh/globalize2">Globalize2</a>. It can be used to chain multiple backends together and will check each backend for a given translation key. This is useful when you want to use standard translations with a <code>Simple</code> backend but store custom application translations in another backends. E.g. you might want to use the <code>Simple</code> backend for managing <a href="http://github.com/svenfuchs/rails-i18n">Rails' internal translations</a> (like ActiveRecord error messages) but use a database backend for your application's translations. To use the <code>Chain</code> backend you can instantiate it and set it to the I18n module. You can then add chained backends through the initializer or backends accessor: <pre><code class="ruby"># preserves an existing Simple backend set to I18n.backend I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)</code></pre> The implementation assumes that all backends added to the <code>Chain</code> implement a lookup method with the same API as <code>Simple</code> backend does. <h2>Cool, what's next?</h2> If you're interested in any of these features, please try these things out and provide some feedback on the <a href="http://groups.google.com/group/rails-i18n">rails-i18n mailinglist</a>. They might make it into the next I18n gem release or not depending on the amount of "real world feedback" we've gotten until then. Also, by now there's <strong>ton</strong> of good stuff that uses or extends I18n to do useful things with it on <a href="http://github.com/search?language=rb&amp;q=i18n&amp;repo=&amp;type=Repositories">Github</a>. I've collected a few things that I found particular suited for being evaluated, maybe merged and potentially included to I18n here: <a href="http://gist.github.com/149905">Interesting I18n repositories</a>. I've also done some work on my i18n-tools repository recently, so you hopefully you can expect some news from that front, too. <h2>Shameless plug</h2> In case you find this stuff useful I'm always happy to receive another recommendation on <a href="http://www.workingwithrails.com/person/9963-sven-fuchs" title="Ruby on Rails developer: Sven Fuchs from Germany, Berlin">working-with-rails</a> :) 2009-07-19T00:00:00Z tag:svenfuchs.com,2011:ruby-i18n-gem-hits-0-2-0 Ruby I18n Gem hits 0.2.0 <p>Here's the release announcement on the <a href="http://groups.google.com/group/rails-i18n">rails-i18n mailinglist</a>:</p> <code> <p>I've bumped the gem version to 0.2.0 and tagged v0.2.0 today.</p> <p>Most importantly it includes:</p> <p>* Lambda support (aka Yaroslav-prevails edition)<br> * Custom separators (aka Gettext-p0wned edition)<br> * Ruby 1.9 interpolation syntax (aka Masao-Mutoh-rocks edition)</p> <p>For more details check out the <a href="http://github.com/svenfuchs/i18n/blob/66e0eb7e1c8fb33fdbf83dbb97609d80be45cb88/CHANGELOG.textile">changelog</a>. </p> <p>Thanks to everybody involved!</p> <p>We still haven't managed to get the inclusion of Rails' vendorized gem right. Thus it's currently not possible to use 0.2.0 in Rails easily.</p> <p>As we've moved from 0.1.x to 0.2.x the pessimistic gem version operator "~> 0.1.3" used in Rails won't load a 0.2.0 version of the gem. I've added a Rails patch that relaxes this to use the optimistic operator ">= 0.1.3" instead.</p> <p>With that patch being applied we'll still have to sort out the Rubyforge release process. Josè Valim has volunteered for this and I've asked Matt to add him to his Rubyforge project.</p> <p>Meanwhile, with above patch applied, we could always clone the Github repo and build the gem manually of course. This should also work fine for playing with experimental builds in the future.</p> </code> <h2>Lambda support</h2> <p>Changesets: <a href="http://github.com/svenfuchs/i18n/commit/e277711b3c844fe7589b8d3f9af0f7d1b969a273">e27771</a>, <a href="http://github.com/svenfuchs/i18n/commit/c90e62d8f7d3d5b78f34cfe328d871b58884f115">c90e62</a>, <a href="http://github.com/svenfuchs/i18n/commit/9d390afcf33f3f469bb95e6888147152f6cc7442">9d390a</a></p> <p>Lambda support has been considered a potential feature from the beginning because it adds the remaining ton of flexibility you need to support wicked things like Russian's irregular date formatting.</p> <p>The shipped simple backend can store lambdas as translations when used with Ruby files. Other backends might implement different mechanisms.</p> <p>E.g. you might store the translation:</p> <pre><code class="ruby">:salutation => lambda { |gender| gender == "m" ? "Mr. {{name}}" : "Mrs. {{name}}" }</code></pre> <p>This will, of course, resolve to either "Mr." or "Mrs." depending on the gender argument passed.</p> <p>This is certainly a power feature and you need to use it wisely. As a rule of thumb make sure your lambdas are cheap and always return the same stuff when passed the same arguments.</p> <h2>Custom scope separators</h2> <p>Changeset: <a href="http://github.com/svenfuchs/i18n/commit/5b75bfbc348061adc11e3790187a187275bfd471">5b75bf</a></p> <p>This feature was a result of a discussion on the Rails core mailinglist about how easy it was to replicate Gettext's behaviour using the I18n gem. The general answer was: It's easy. But the nitty-gritty details are that I18n uses a period/dot as a scope separator so you can not use a full sentence as both a key and default value &#8211; as Gettext uses to do that.</p> <p>So we've added support for customizing the scope separator both globally and on a per-request basis. You can:</p> <pre><code class="ruby"> # set a different scope separator globally: I18n.default_separator ="\001" I18n.t("Foo. And bar, too.") # pass it as an option to #translate: I18n.t("Foo. And bar, too.", :separator => "\001") </code></pre> <p>Yes. Of course the I18n API totally allows you to use Strings as keys. It will make more sense if you <a href="http://github.com/svenfuchs/i18n/blob/fb7fcfff5e94510dbc1cb0b9b12a374c6828fb6f/lib/i18n/gettext.rb#L2">add a Gettext-like helper</a> to access the backend though.</p> <h2>Symlinking translations</h2> <p>Changeset: <a href="http://github.com/svenfuchs/i18n/commit/8c4ce3d923ce5fa73e973fe28217e18165549aba">8c4ce3</a></p> <p>After we've implemented the mentioned lambda support we've refactored some portions of Simple backend and and cleaned up the implementation. When we were done we accidentally noticed that we've also implemented support for a previously requested feature that we refer to as "symlinking translations".</p> <p>So far we're not completely convinced that this should be made an official feature so we did not document it in the I18n module docs, yet. On the other hand people have asked for it and there's no reason to officially "disallow" it either. So we'll just mention it here and let people decide :)</p> <p>You can now symlink translations by returning a Ruby Symbol from either your literal translation data or computed lambdas. So, for example:</p> <pre><code class="ruby"> # yaml translation data actions: edit: Edit articles: actions: edit: :"actions.edit" I18n.t(:"articles.actions.edit") # => "Edit" </code></pre> 2009-07-12T00:00:00Z tag:svenfuchs.com,2011:ripper2ruby-modify-and-recompile-your-ruby-code Ripper2Ruby: modify and recompile your Ruby code <p>So, the combination Ripper/Ripper2Ruby lets you do similar things as you can do with <a href="http://parsetree.rubyforge.org" title="Seattle.rb - parse_tree and ruby_parser">ParseTree or RubyParser</a> and <a href="http://blog.zenspider.com/2005/02/rubytoruby.html" title="RubyToRuby - Polishing Ruby">Ruby2Ruby</a>. The differences are:</p> <ul> <li>Ripper requires Ruby 1.9 (I was told it possibly could be compiled to work with Ruby 1.8.x but I don&#8217;t know anything further. Please drop me a note if you know how to do this.)</li> <li>Ripper2Ruby builds a full object-oriented representation of Ruby code. That means you can modify the representation much more easily compared to the rough sexp tree that you get from the parsers. It also provides complete information about the node&#8217;s original source position, whitespace, comments etc.</li> <li>Therefor with Ripper2Ruby you can recompile the exact copy of the original source code, character by character (that&#8217;s not possible with Ruby2Ruby). Ripper2Ruby has been tested with <a href="http://gist.github.com/137398" title="gist: 137398 - GitHub">225 Ruby libraries</a> including Rails, Merb, Ruby Stdlib etc.</li> <li>Ripper2Ruby does more but it&#8217;s slower, too.</li> </ul> <p>For example:</p> <pre><code class="ruby"> src = "I18n.t(:foo)" code = Ripper::RubyBuilder.build(src) code.to_ruby # =&gt; "I18n.t(:foo)" foo = code.select(Ruby::Symbol).first foo.identifier.token = 'bar' code.to_ruby # =&gt; "I18n.t(:bar)" </code></pre> <p>Ripper2Ruby was build to make it easier to create refactoring tools for Ruby/Rails I18n support (see i18n-tools). Huge thanks go (again) to <a href="http://bestgroup.eu">Torsten Becker, Bestgroup Software &amp; Consulting</a> for making this possible.</p> 2009-07-05T00:00:00Z tag:svenfuchs.com,2011:using-ruby-1-9-ripper Using Ruby 1.9 Ripper <p>While Ripper parses your code it continously fires events (or &#8220;calls callbacks&#8221;) when it finds something interesting. There are two types of events: scanner (lexer) and parser events.</p> <p>The scanner basically goes through the code from the left to the right character by character. When it finds known things (such as a keyword, whitespace or a semicolon) it fires a corresponding even that you can react to. The parser works on a higher level and watches for known Ruby constructs (such as a symbol, a method call or a class definition) and also fires events.</p> <p>You can check the available events by outputting <code><a href="http://github.com/svenfuchs/ripper2ruby/blob/303d7ac4dfc2d8dbbdacaa6970fc41ff56b31d82/notes/scanner_events" title="notes/scanner_events at 303d7ac4dfc2d8dbbdacaa6970fc41ff56b31d82 from svenfuchs's ripper2ruby - GitHub">Ripper::SCANNER_EVENTS</a></code> and <code><a href="http://github.com/svenfuchs/ripper2ruby/blob/303d7ac4dfc2d8dbbdacaa6970fc41ff56b31d82/notes/parser_events" title="notes/parser_events at 303d7ac4dfc2d8dbbdacaa6970fc41ff56b31d82 from svenfuchs's ripper2ruby - GitHub">Ripper::PARSER_EVENTS</a></code>. </p> <p>You can respond to these events by simply defining methods named <code>:"on_#{event_name}"</code> (omitting the <code>@</code> character for scanner events). As long as you do not mess this up (which you might want to do) the parser always passes the results from the last inner parser events to the current parser event. E.g.:</p> <pre><code class="ruby">require 'ripper' class DemoBuilder &lt; Ripper::SexpBuilder def on_int(token) # scanner event super.tap { |result| p result } end def on_binary(left, operator, right) # parser event super.tap { |result| p result } end end src = "1 + 1" DemoBuilder.new(src).parse </code></pre> <p>This outputs:</p> <pre><code class="ruby">[:@int, "1", [1, 0]] [:@int, "1", [1, 4]] [:binary, [:@int, "1", [1, 0]], :+, [:@int, "1", [1, 4]]] </code></pre> <p>When a scanner event is fired you can check the current position (it is passed to the event but you can also always call <code>self.position</code>) which allows for tracking detailled positioning information. Positions are given as [row, column] with the row being 1-based. On parser level events the current position is not very useful (and not passed to your event callbacks) because parser events are fired when the parser recognizes a known ruby construct as completed - i.e. at the end of the construct.</p> <p>Scanner events are fired &#8220;just so&#8221;, i.e. the scanner finds something and calls your callback method. The return values might or might not be passed to parser events. Parser events otoh build a meaningful tree and their return values are always passed to the next (outer) event. You can generally think of events being fired &#8220;from the inside out&#8221;, starting with lowlevel scanner events.</p> <p>You can examine the hierarchie of these events by doing:</p> <pre><code class="ruby">require "pp" src = "1 + 1" pp Ripper::SexpBuilder.new(src).parse </code></pre> <p>will output:</p> <pre><code class="ruby"> [:program, [:stmts_add, [:stmts_new], [:binary, [:@int, "1", [1, 0]], :+, [:@int, "1", [1, 4]]]]] </code></pre> <p>You think of this as a nested method call where the first element of each array is the method name and the rest are the arguments. In the example above there would be 5 method calls. The first <code>:@int</code> call would receive the arguments <code>"1"</code> and <code>[1, 0]</code>, the <code>:binary</code> would receive <code>["1", [1, 0]], :+, ["1", [1, 4]]</code>. The other calls, like <code>:program</code> would not receive any arguments.</p> <p>When executed the (theoretical) interpreter would first evaluate the innermost arguments, right? That&#8217;s exactly what Ripper does, too. It will first fire the first @int event, then the second one and then pass the return values of these two events (together with the <code>:+</code> operator token) to the next outer method, which is the <code>:binary</code> event in this case.</p> <p>(&#8220;Theoretical&#8221; of course refers to these particular s-expressions. There are languages that are very much based on exactly this concept, like e.g. Lisp.)</p> <p>As you can see even though the scanner fires events on whitespace there aren&#8217;t any whitespace characters passed to any of the callbacks. I don&#8217;t know if there&#8217;s anything else happening to these but of course you can define callbacks for the different kinds of whitespace and do something useful with it. The same is true for comments and quite some stuff that doesn&#8217;t make a semantical difference in Ruby (such as parentheses for method calls etc.).</p> <p>To examine all events in the order they are actually fired you can use the event log that ships with Ripper2Ruby:</p> <pre><code class="ruby"> src = "1 + 1" Ripper::EventLog.out(src) </code></pre> <p>will output:</p> <pre><code class="ruby"> @int 1 @sp " " @op + @sp " " @int 1 binary stmts_new stmts_add program </code></pre> <p>I&#8217;m not an expert here but Ripper&#8217;s s-expressions and events seemed to make more sense to me than ParseTree&#8217;s stuff. Ripper still doesn&#8217;t seem to be completely consistent though. </p> <p>E.g. for word lists (i.e. Arrays that are defined using <code>%w()</code> syntax) there are different events fired depending whether you have <code>%w()</code> or <code>%W()</code>.</p> <pre><code class="ruby">src = '%W(foo bar)' pp Ripper::SexpBuilder.new(src).parse </code></pre> <p>outputs:</p> <pre><code class="ruby"> [:program, [:stmts_add, [:stmts_new], [:words_add, [:words_add, [:words_new], [:word_add, [:word_new], [:@tstring_content, "foo", [1, 3]]]], [:word_add, [:word_new], [:@tstring_content, "bar", [1, 7]]]]]] </code></pre> <p>But on the other hand:</p> <pre><code class="ruby"> src = '%w(foo bar)' pp Ripper::SexpBuilder.new(src).parse </code></pre> <p>outputs:</p> <pre><code class="ruby"> [:program, [:stmts_add, [:stmts_new], [:qwords_add, [:qwords_add, [:qwords_new], [:@tstring_content, "foo", [1, 3]]], [:@tstring_content, "bar", [1, 7]]]]] </code></pre> <p>As you can see for qwords (i.e. the non-interpolating version) there seems to be a <code>:qwords_add</code> and <code>:qwords_new</code> event missing. I can&#8217;t see any good reason for this.</p> <p>Also, Ripper seems to get the method call operator wrong when you use <code>"::"</code></p> <pre><code class="ruby"> src = "A::b()" pp Ripper::SexpBuilder.new(src).parse </code></pre> <p>outputs:</p> <pre><code class="ruby"> [:program, [:stmts_add, [:stmts_new], [:method_add_arg, [:call, [:var_ref, [:@const, "A", [1, 0]]], :".", [:@ident, "b", [1, 3]]], [:arg_paren, nil]]]] </code></pre> <p>Watch the period which should be a <code>:"::"</code> symbol.</p> <p>In quite some situations I&#8217;ve found the events ambigous or not explicit. E.g. for the closing parentheses in a words list like <code>%w(foo bar)</code> Ripper fires a <code>:@tstring_end</code> event - which is the same event as it fires for closing parentheses in Strings as in <code>%(foobar)</code>.</p> <p>It gets really weird when you try to build something from the events that Ripper fires for <a href="http://github.com/svenfuchs/ripper2ruby/blob/303d7ac4dfc2d8dbbdacaa6970fc41ff56b31d82/test/nodes/heredoc_test.rb" title="test/nodes/heredoc_test.rb at 303d7ac4dfc2d8dbbdacaa6970fc41ff56b31d82 from svenfuchs's ripper2ruby - GitHub">Heredocs</a> or even stacked Heredocs combined with method calls on the Heredoc opener token - maybe the most weird Ruby construct anyway. In general though this stuff is fun to work with and quite obvious once you got the idea :)</p> 2009-07-05T00:00:00Z tag:svenfuchs.com,2011:rails-i18n-revs-up-globalize2-preview-released Rails I18n revs up: Globalize2 preview released! <p style="padding:10px;background-color: #efefef;border: 1px solid #555;"> Please note: the following explanations assume that you're familiar with the <a href="http://www.artweb-design.de/2008/7/18/the-ruby-on-rails-i18n-core-api">new I18n API in Rails</a> and might leave some unanswered questions otherwise :-). Also note that this is a preview release targeted at Rails I18n developers. We'll do at least one more release and provide more complete documentation about how Globalize2 can be used by end users then. </p> <h2>Globalize2 preview</h2> <p>The first preview release of Globalize2 includes the following features and tools. Most of them can be used independent of each other so you can pick whatever tools you need and combine them with other libraries or plugins.</p> <ul> <li><strong>Model translations</strong> &#8211; transparently translate ActiveRecord data</li> <li><strong>Static backend</strong> &#8211; swap the Simple backend for this more powerful backend, enabling custom pluralization logic, locale fallbacks and translation value objects</li> <li><strong>Locale LoadPath</strong> &#8211; easily load translation data from standard locations enforcing conventions that suite your needs</li> <li><strong>Locale Fallbacks</strong> &#8211; make sure your translation lookups fall back transparently through a path of alternative locales that make sense for any given locale in your application</li> <li><strong>Translation value objects</strong> &#8211; access useful meta data information on the translations returned from your backend and/or translated models</li> </ul> <p>Also, we've put together a small and simple demo application for demonstrating Globalize2's feature set. You can find <a href="http://github.com/joshmh/globalize2-demo">globalize2-demo</a> on GitHub. Instructions for installation are included in the readme at the bottom of that page.</p> <p>The implementation of Globalize2 has been sponsored by the company <a href="http://best-group.eu"><em>BEST</em>Group consulting and software</a>, Berlin.</p> <h2>Installation</h2> <p>To install Globalize2 with its default setup just use:</p> <pre><code class="ruby"> script/plugin install -r 'tag 0.1.0_PR1' git://github.com/joshmh/globalize2.git </code></pre> <p>This will:</p> <ul> <li>activate model translations</li> <li>set I18n.load_path to an instance of Globalize::LoadPath</li> <li>set I18n.backend to an instance of Globalize::Backend::Static</li> </ul> <h2>Configuration</h2> <p>You might want to add additional configuration to an initializer, e.g. config/initializers/globalize.rb</p> <h2>Model translations</h2> <p>Model translations (or content translations) allow you to translate your models&#8217; attribute values. E.g.</p> <pre><code class="ruby"> class Post &lt; ActiveRecord::Base translates :title, :text end </code></pre> <p>Allows you to translate values for the attributes :title and :text per locale:</p> <pre><code class="ruby"> I18n.locale = :en post.title # Globalize2 rocks! I18n.locale = :he post.title # ?????????2 ????! </code></pre> <p>In order to make this work you currently need to take care of creating the appropriate database migrations manually. Globalize2 will provide a handy helper method for doing this in future.</p> <p>The migration for the above Post model could look like this:</p> <pre><code class="ruby"> class CreatePosts &lt; ActiveRecord::Migration def self.up create_table :posts do |t| t.timestamps end create_table :post_translations do |t| t.string :locale t.references :post t.string :title t.text :text t.timestamps end end def self.down drop_table :posts drop_table :post_translations end end </code></pre> <h2>Globalize::Backend::Static</h2> <p>Globalize2 ships with a Static backend that builds on the Simple backend from the I18n library (which is shipped with Rails) and adds the following features:</p> <ul> <li>It uses locale fallbacks when looking up translation data.</li> <li>It returns an instance of Globalize::Translation::Static instead of a plain Ruby String as a translation.</li> <li>It allows to hook in custom pluralization logic as lambdas.</li> </ul> <h2>Custom pluralization logic</h2> <p>The Simple backend has its pluralization algorithm baked in hardcoded. This algorithm is only suitable for English and other languages that have the same pluralization rules. It is not suitable for, e.g., Czech though.</p> <p>To add custom pluralization logic to Globalize&#8217; Static backend you can do something like this:</p> <pre><code class="ruby"> @backend.add_pluralizer :cz, lambda{|c| c == 1 ? :one : (2..4).include?(c) ? :few : :other } </code></pre> <h2>Locale Fallbacks</h2> <p>Globalize2 ships with a Locale fallback tool which extends the I18n module to hold a fallbacks instance which is set to an instance of Globalize::Locale::Fallbacks by default but can be swapped with a different implementation.</p> <p>Globalize2 fallbacks will compute a number of other locales for a given locale. For example:</p> <pre><code class="ruby"> I18n.fallbacks[:"es-MX"] # =&gt; [:"es-MX", :es, :"en-US", :en] </code></pre> <p>Globalize2 fallbacks always fall back to</p> <ul> <li>all parents of a given locale (e.g. :es for :"es-MX"), </li> <li>then to the fallbacks&#8217; default locales and all of their parents and </li> <li>finally to the :root locale.</li> </ul> <p>The default locales are set to [:"en-US"] by default but can be set to something else. The root locale is a concept borrowed from <a href="http://unicode.org"><span class="caps">CLDR</span></a> and makes sense for storing common locale data which works as a last default fallback (e.g. "ltr" for bidi directions).</p> <p>One can additionally add any number of additional fallback locales manually. These will be added before the default locales to the fallback chain. For example:</p> <pre><code class="ruby"> fb = I18n.fallbacks fb.map :ca =&gt; :"es-ES" fb[:ca] # =&gt; [:ca, :"es-ES", :es, :"en-US", :en] fb.map :"ar-PS" =&gt; :"he-IL" fb[:"ar-PS"] # =&gt; [:"ar-PS", :ar, :"he-IL", :he, :"en-US", :en] fb[:"ar-EG"] # =&gt; [:"ar-EG", :ar, :"en-US", :en] fb.map :sms =&gt; [:"se-FI", :"fi-FI"] fb[:sms] # =&gt; [:sms, :"se-FI", :se, :"fi-FI", :fi, :"en-US", :en] </code></pre> <h2>Globalize::LoadPath</h2> <p>Globalize2 replaces the plain Ruby array that is set to I18n.load_path by default through an instance of Globalize::LoadPath.</p> <p>This object can be populated with both paths to files and directories. If a path to a directory is added to it it will look up all locale data files present in that directory enforcing the following convention:</p> <pre><code class="ruby"> I18n.load_path &lt;&lt; "#{RAILS_ROOT}/lib/locales" # will load all the following files if present: lib/locales/all.yml lib/locales/fr.yml lib/locales/fr/*.yaml lib/locales/ru.yml lib/locales/ru/*.yaml ... </code></pre> <p>One can also specify which locales are used. By default this is set to "*" meaning that files for all locales are added. To define that only files for the locale :es are added one can specify:</p> <pre><code class="ruby"> I18n.load_path.locales = [:es] </code></pre> <p>One can also specify which file extensions are used. By default this is set to ["rb", "yml"] so plain Ruby and <span class="caps">YAML</span> files are added if found. To define that only *.sql files are added one can specify:</p> <pre><code class="ruby"> I18n.load_path.extensions = ['sql'] </code></pre> <p>Note that Globalize::LoadPath &#8220;expands&#8221; a directory to its contained file paths immediately when you add it to the load_path. Thus, if you change the locales or extensions settings in the middle of your application the change won&#8217;t be applied to already added file paths.</p> <h2>Globalize::Translation classes</h2> <p>Globalize2&#8217;s Static backend as well as Globalize2 model translations return instances of Globalize::Translation classes (instead of plain Ruby Strings). These are simple and lightweight value objects that carry some additional meta data about the translation and how it was looked up.</p> <p>Model translations return instances of Globalize::Translation::Attribute, the Static backend returns instances of Globalize::Translation::Static.</p> <p>For example:</p> <pre><code class="ruby"> I18n.locale = :de # Translation::Attribute title = Post.first.title # assuming that no translation can be found: title.locale # =&gt; :en title.requested_locale # =&gt; :de title.fallback? # =&gt; true # Translation::Static rails = I18n.t :rails # assuming that no translation can be found: rails.locale # =&gt; :en rails.requested_locale # =&gt; :de rails.fallback? # =&gt; true rails.options # returns the options passed to #t rails.plural_key # returns the plural_key (e.g. :one, :other) rails.original # returns the original translation with no values # interpolated to it (e.g. "Hi {{name}}!") </code></pre> <h2>Other notes</h2> <p>Please note that the Globalize2 Static backend (just like the Simple backend) does not support reloading translation data.</p> 2008-09-19T00:00:00Z tag:svenfuchs.com,2011:the-future-of-i18n-in-ruby-on-rails-railsconf-europe-2008 The Future of I18n in Ruby on Rails - RailsConf Europe 2008 <style> #talk img { width: 100%; border: 4px solid #ddd; } #talk p { margin-top: 1em !important; margin-bottom: 1em !important; font-size: 18px !important; line-height: 140% !important; } </style> <div id="talk"> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.001.png"> <p>Welcome everybody!</p> <p>I'm very happy to talk about "The future of Internationalization in Ruby on Rails" today.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.004.png"> <p>So, who&#x2019;s me?</p> <p>This never occurred to me before but in a recent RailsEnvy podcast I&#x2019;ve learned that my name could be pronounced like this ...</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.005.png"> <p>If you want to google me ...</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.006.png"> <p>... you probably have better luck using this.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.010.png"> <p>I'm currently living in Berlin and I love it :)</p> <p>I started programming in 1984</p> <p>I've been in business as a developer, mostly as a web developer for some years and switched to working with Ruby and then Ruby on Rails a couple of years ago.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.014.png"> <p>Some of you might know me because I've led the Rails I18n group for the last couple of months and did the final touches on the implementation of the new API in Rails recently.</p> <p>Some people know my blog because of some tutorials I've written about Globalize.</p> <p>And I&#x2019;m currently working on a CMS application platform called adva_cms.</p> <p>Of course, as a German developer I've always been interested in support for Internationalization so I'm really happy that I’ve able to contribute something in this area to Ruby on Rails.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.016.png"> <p>If there are any questions please try to make a mental note and keep it for the end. I've set some time aside for Q&A at the end of the talk.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.023.png"> <p>Ok, by the end of this talk I hope you&#x2019;ll have a clear picture of what the Rails Internationalization project is, what&#x2019;s included in the new Rails Internationalization API and what&#x2019;s not and how you can use it.</p> <p>I will give a very brief demo. Of course this is a huge topic so I&#x2019;ll point you to where you can find more resources.</p> <p>Then we have a short and sweet Interview video prepared with Joshua Harvey the original author of Globalize</p> <p>And finally I&#x2019;ll give you some hints about what we&#x2019;re going to do in the future and how you can contribute to that.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.024.png"> <p>So, to better understand the future of Internationalization in Rails I want to contrast that a bit with the history that we all suffered through. As you know the history of Internationalization in Rails is really, really dark.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.029.png"> <p>These were times of chaos. </p> <p>They were unclear, unpredictable, unstable and highly adventurous.</p> <p>You'll see the reason for that in a minute. Let me first let you show how many different solutions we've already seen and how wide their variety is.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.030.png"> <p>We have seen things like ...</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.038.png"> <p>Ruby Gettext - the dinosaur and grandfather of Internationalization</p> <p>David&#x2019;s original Localization plugin as well as ...</p> <p>Thomas Fuchs&#x2019; Localization plugin - both very simple</p> <p>GLoc - one of the first bigger implementations</p> <p>Globalize - the only solution with solid Model translations</p> <p>Globalite - a very slick solution by Matt Aimonetti </p> <p>Simple Localization - also a very interesting solution by Stephan Soller</p> <p>Both have contributed a lot to the Rails I18n project by the way.</p> <p>Gibberish - maybe the most recent full solution, came along as the cool kid on the block.</p> <p>... and many more. </p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.039.png"> <p>There are solutions for special needs like ...</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.042.png"> <p>Localizing Rails to like Swedish, Brazilian, Korean and so on.</p> <p>There are plugins that add functionality to other plugins. For example there is Jibberish that adds a Javascript Layer on top of Gibberish. And things that just adds separate, useful tools.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.043.png"> <p>Stuff like that ... of course there&#x2019;s much more. I just wanted to give you an impression of these things.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.044.png"> <p>So when you take into account how complex our languages are and how many funny problems there are to solve and we have lots of lonely programmers working on this and everybody focusses on the features he actually needs himself ...</p> <p>What do you think will you get? </p> <p>What you get is a huge mess.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.048.png"> <p>We have lots of isolated, incompatible solutions.</p> <p>Things like support and documentation just don&#x2019;t get better with a lonely developer solving their own needs and then - of course - often walking away and focussing on something completely different.</p> <p>Then, all of our solutions basically were huge Monkeypatches to Rails with the usual problems resulting from that.</p> <p>And all of us re-implemented the wheel in one way or the other. So, in a way, we wasted lots of resources.</p> <p>But - on the other hand - this situation was actually necessary for us to experiment with solutions and learn how Rails Internationalization could be done.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.054.png"> <p>Because of this situation last year in September a group of developers from several Rails Internationalization plugins gathered for a chat session. We agreed that we wanted to implemented a Rails core patch which should solve the problems we had with this stuff.</p> <p>We invited developers from all existing solutions that we knew and started meeting regulary, collected requirements, discussed our ideas ...</p> <p>Actually there was really a lot of discussion and initially it was hard to agree on anything. </p> <p>You know that sort of situation. Everybody brings their own favorite little features to the table, everybody has different beliefs of what's good and what's necessary. So this discussion went on endlessly and in the beginning of 2008 our work slowed down. Apparently we really needed a creative break and some time to rethink everything.</p> <p>Then, in May 2008 we got back to the drawing board with a fresh approach ...</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.059.png"> <p>... and now we were able to agree on a very slim and lightweight implementation that covered all of our requirements</p> <p>It works as a Ruby gem which means it is not only suited for use in Rails but also other frameworks.</p> <p>We wanted only minimal changes in Rails, we wanted it to add as less load to Rails as possible and of course we still wanted it to implement a common API where everybody could build on.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.060.png"> <p>So, at this point Jeremy Kemper didn&#x2019;t really know about our work but in a RailsConf Portland RailsEnvy interview he predicted for Rails 2.2 that ...</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.061.png"> <p>... Internationalization will be solved.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.062.png"> <p>When we asked Jeremy about this he said it was a joke</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.063.png"> <p>So we think Jeremy should definitely crack more jokes about things like this.</p> <p>Because in the end this joke actually got us going to finally propose our work to the core team.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.067.png"> <p>In hindsight there were a few key decisions. Before we managed to agree on these we had not been able to get anything really done. These key decisions were:</p> <p>We agreed to make the actual implementation of all the logic exchangeable, so you can always plug in something more powerful or more flexible if you need to.</p> <p>We radically sticked to the principle of &#x201C;doing the simplest thing that ever could work&#x201D; for Rails.</p> <p>And maybe most importantly we just stopped aiming for a solution that did anything else than the bare minimum that’s needed for Rails.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.070.png"> <p>So, what we now ship with Rails is a common API for Internationalization which is basically just a bunch of definitions how things are supposed to work. So this is just a Ruby module with some public methods on that.</p> <p>These methods delegate to a backend that actually implements the API. The backend that is shipped with Rails is intentionally named the Simple backend because the only requirement for the Implementation in the Simple backend is that it works for American English because that’s what we need for Rails to keep it working like it worked before.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.072.png"> <p>That means: the most important parts that previously were hardcoded right into Rails are now abstracted out of the code and moved to a simplistic backend, accessible through just a few public methods.</p> <p>So this is true</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.074.png"> <p>While this ... might work. Maybe you can localize Rails to your favorite language if it is very similar to English. There are quite some resources out there that show how to do this. For everything else we still need to add plugins or other libraries.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.075.png"> <p>For Rails itself we settled with shipping the translations as YAML files and with stock Rails you can also use Ruby files, so you can have lambdas in your translation data.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.076.png"> <p>I&#x2019;ll run through the main features that this API provides.</p> <p>As I said the API is provided as a Ruby module with two main methods on it.</p> <p>These are:</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.078.png"> <p>I18n.translate which you can use to look up translations from the backend and for convenience it has an alias #t and you need to pass it at least one parameter which is the key.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.079.png"> <p>So this would look up the translation that&#x2019;s stored for the key :rails in the current locale, for example, by default American English.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.080.png"> <p>The other method that you&#x2019;ll use regularly is localize.</p> <p>This one basically allows you to format Date and Time objects for a certain locale.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.081.png"> <p>Again, for convenience there&#x2019;s an alias ... of course that&#x2019;s #l and you pass it the object that you want to localize</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.084.png"> <p>So the #translate gives you very powerful means to look up translations.</p> <p>You can look up your translations using a Symbol as a key and this is what we&#x2019;d generally encourage you to do. But you can also use a String. </p> <p>Also, you have scopes, so you can namespace your translations and structure them in a reasonable way.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.089.png"> <p>For example in the ActiveRecord translations YAML file there&#x2019;s a scope activerecord and then there&#x2019;s a scope errors, a scope messages and finally there&#x2019;s finally the translation for the key :invalid.</p> <p>So scopes are basically groups or namespaces of translations. You can think of them as nested Hashes</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.090.png"> <p>Consequently there&#x2019;s a way to look up whole namespaces - or groups - of translations. For example this will look up all error messages in ActiveRecord and return a Hash with all those messages.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.091.png"> <p>Then there&#x2019;s bulk lookup. That just means that you can pass an Array of keys you want translated and it will return you a corresponding array of translations.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.093.png"> <p>And finally, of course, there&#x2019;s defaults so you can specify what happens when a translation can not be found.</p> <p>For example when you give it a String as a default and the translation can not be found it just returns the String. </p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.094.png"> <p>But when you give it a Symbol it will translate this Symbol and give you that translation as a default. </p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.095.png"> <p>Finally you can specify an array of Symbols and/or Strings and it will just check everything in that order and return the first value that&#x2019;s not nil.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.099.png"> <p>Interpolation is another feature.</p> <p>That means you want to abstract your translations in certain situations. For example you have a translation for the key :message and there&#x2019;s variable :name in that. Then you can pass the value of that variable to #translate and it will parse the value into the translation.</p> <p>This looks trivial but it&#x2019;s actually a very important feature because concatenating translations is kind of an anti-pattern in many situations.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.104.png"> <p>Also, the API provides means to define Singular and Plural translations of course.</p> <p>For example you could have a Singular and a Plural translations like this. Then you can pass a count option to #translate. So in this case it would pick the Singular translation because count is 1. And when you pass it 2 it would pick the Plural translation.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.105.png"> <p>So that&#x2019;s it already - all very basic stuff of course. No worries, you can look up everything online :-)</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.106.png"> <p>Like I said, the I18n API ships with a Simple backend ...</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.107.png"> <p>... which implements all of these features for American English only. So for example you can&#x2019;t use it to do pluralization for Czech because they have different rules for pluralization.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.109.png"> <p>As I just showed you your English translations include something like this.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.113.png"> <p>But in Czech you have two plural forms - one of them is called Paucal. They use the singular just like one does in English. Then they use the Paucal, the second form, for everything between 2 and 4. And the actual plural for everything else.</p> <p>So the Simple backend can not do that but we can easily swap it and the Internationalization API actually encourages you to do so.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.117.png"> <p>You would just create a class for your custom backend and maybe extend the Simple Backend, implement your custom logic and tell the I18n module to use it.</p> <p>That&#x2019;s it.</p> <p>Basically this is what future plugins will do a lot. For example there definitely will be a backend that stores translations to the database and I’m sure there’s one soon to read and store them from and to Gettext files.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.118.png"> <p>So let&#x2019;s have a really brief look how you can use this in your application.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.119.png"> <p>So, if you&#x2019;re actually working on an application today, what can use use?</p> <p>Of course this really depends on your requirements. So let&#x2019;s have a look at some scenarios ...</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.120.png"> <p>Let’s say you just need a few static translations and maybe some date and number formats. Then you can easily reuse the Simple backend that Rails ships with. By now there are a couple of demo applications on GitHub that show how to do that.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.121.png"> <p>Let&#x2019;s say you have a language that requires different pluralization rules. Then there&#x2019;s an experimental Pluralizing backend for that where you can hook in pluralization logic as lambdas.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.122.png"> <p>Let&#x2019;s say you need to translate your application to Russian, or Czech or another language that has some more complex date formats.</p> <p>For that there&#x2019;s an experimental Chain backend where you could hook in a custom backend tailored for just these languages&#x2019; date formats.</p> <p>Also there&#x2019;s already a full plugin solution by Yaroslav Markin from Russia that implements things like these and I think it&#x2019;s easy to adapt it for other languages.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.123.png"> <p>If you need model translations there&#x2019;ll be Globalize2. Globalize2 will look completely different to Globalize. It will be pretty lightweight and unobtrusive. Joshua will tell you more about that in the interview.</p> <p>Also there&#x2019;s an alternative approach, that&#x2019;s linked on rails-i18n.org</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.124.png"> <p>So, if we still have to rely on external code, like plugins, to localize our applications. </p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.125.png"> <p>What do we win?</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.126.png"> <p>The short answer to that clearly is: we win a lot!</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.127.png"> <p>First of all, we don&#x2019;t need no monkeypatching any more </p> <p>Well, of course we still will monkeypatch Rails to experiment with new features.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.132.png"> <p>But we definitely won't need to do probably 99% of what we&#x2019;ve done before like translating ActiveRecord error messages, translating all the helpers that Rails provides or anything like that.</p> <p>Also, because we now can build on a common API we expect things to become much better exchangeable.</p> <p>We will aggregate common locale data over time. So we will build up a Rails specific pool of Localization knowledge.</p> <p>Also we hope that plugins will be much more focussed on implementing certain atomic features.</p> <p>This is actually something we encourage everybody to do. You know, so far, you could not possibly combine three, four, five different plugins and expect anything to work. We hope we’re now able to do a better job here in future.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.133.png"> <p>So, of course, with all of this stuff released our next development cycle has started ... right now.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.138.png"> <p>Of course we&#x2019;re all going to implement our stuff next. In fact by now there are already quite some useful things out there. During this time we will try to stay in touch with each other and then, again, collect everything, review it, again, discuss it and finally aim for another core patch.</p> <p>The reason why we want to first collect everything and look at it as a whole and then move it to Rails as a rather complete package is that many of these things make most sense in context with each other.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.144.png"> <p>We hope this will then result in a landscape of Rails Internationalization solutions where we have a distributed toolbox of focussed plugins rather than those huge solutions trying to solve everything under the sun at once that we had before.</p> <p>We&#x2019;ll have more compatible solutions and maybe the most exciting thing is that we will get to have Rails specific, common locale data.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.145.png"> <p>So, let&#x2019;s see what Joshua Harvey has to say about Globalize 2.</p> <object width="600" height="475"><param name="movie" value="http://www.youtube.com/v/OXxUF_N-3-M&hl=en&fs=1"></param><param name="allowFullScreen" value="true"></param><embed src="http://www.youtube.com/v/OXxUF_N-3-M&hl=en&fs=1" type="application/x-shockwave-flash" allowfullscreen="true" width="600" height="475"></embed></object> <p>By the way the guitar shop that you can see in the video is actually the shop rashgash and the website at rashgash.com is the reason why Joshua initially wrote Globalize as he mentions in the interview.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.146.png"> <p>So this is a brief overview of features that Globalize 2 will provide.</p> <p>Of course there&#x2019;s model translations - kind of Globalize&#x2019;s USP in the past. For View translations we&#x2019;ll basically use the Rails I18n API. There will be some useful tools like standards compliance tool for locales which we&#x2019;ll need for locale fallbacks and stuff like that. A locale load_path tool that helps loading translation data more easily.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.148.png"> <p>Actually the implementation of Globalize 2 is sponsored by the company BESTGroup Consulting &amp; Software from Berlin. So, thank you, BESTGroup for making this possible.</p> <p>Also, Metaversum, a company also from Berlin will start sponsoring us right after this conference. So, thank you, Metaversum in advance.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.149.png"> <p>And actually we are looking for more sponsors not only for the Globalize project but also for the Rails I18n project because we really want to put this project on a really solid foundation. </p> <p>For example we want to have somebody working on stuff like documentation on a regular basis and we want to provide better support for newbies on the mailing list and other stuff that is all usually much easier to do when you have a small budget to spend.</p> <p>So if you&#x2019;re interested in sponsoring a really cutting edge and high-profile Ruby on Rails Open Source project that already gets tons of attention and definitely will get even more attention in the next couple of months ... just catch me afterwards and we can talk about that.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.151.png"> <p>So, how can everybody in this room get involved?</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.156.png"> <p>Of course everybody can just start playing with all of this, register to our mailinglist and give feedback. We need tons of that.</p> <p>You can share your translations. We already have a repository on GitHub for that, so if you&#x2019;ve done translations for Rails please submit them.</p> <p>You can build plugins focussed on certain features that are still missing. And please don&#x2019;t forget to pitch them on the mailinglist and our wiki.</p> <p>You can help improving the Rails integration. You know Rails helpers are there and they are working. But that doesn&#x2019;t mean they&#x2019;re already perfect regarding Internationalization. So you can help to improve that.</p> <p>And of course, you can sponsor our work so we can invest some money in things that are hard to do otherwise.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.157.png"> <p>So, basically this was my talk. We&#x2019;ve collected pretty much everything I mentioned in this talk as well as lots of other resources on <a href="http://rails-i18n.org" title="Rails I18n">http://rails-i18n.org</a>. So, look it up.</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.158.png"> <p>Thanks to everybody who contributed!</p> <img src="http://sven.artweb-design.de/railsconf-europe-08/i18n/images/rails-i18n.159.png"> <p>Ok, so what are all those questions you want to ask?</p> </div> 2008-09-06T00:00:00Z tag:svenfuchs.com,2011:ruby-on-rails-i18n-railsconf-europe-and-globalize2 Ruby on Rails I18n, RailsConf Europe and Globalize2 <h2>RailsConf Europe talk: "The future of I18n in Ruby on Rails"</h2> <a href="http://www.railsconfeurope.com" style="float: right; margin: 0px 0px 10px 20px; border-width: 0px;"><img src="http://assets.en.oreilly.com/1/event/13/railseurope2008_spk_125x125.gif" width="125" height="125" border="0" alt="RailsConf Europe 2008" title="RailsConf Europe 2008" /></a> <p>I'll be giving a talk about "<a href="http://en.oreilly.com/railseurope2008/public/schedule/detail/3569" title="The Future of I18n/L10n in Ruby on Rails: RailsConf Europe 2008 - O'Reilly Conferences, 02 September - 04, 2008, Berlin, Germany">The future of I18n in Ruby on Rails</a>" on <a href="http://en.oreilly.com/railseurope2008/public/content/home" title="RailsConf Europe 2008 - O'Reilly Conferences, 02 September - 04, 2008, Berlin, Germany">RailsConf Europe</a> next week together with <a href="http://www.workingwithrails.com/person/10731-marko-sepp">Marko Seppae</a>.</p> <p>Am I excited about it? You bet! Will that be an interesting talk for everyone involved into Rails I18n? Of course, I'm sure :)</p> <p>Also, there are some plans to also work on I18n/Rails during the Code Community Drive but I'm not sure what the status of this is right now.</p> <p>I'm personally planning to take care of another project in the same context already: <a href="http://adva-cms.org/" title="adva cms">adva-cms</a>. So I might not be able to put a great amount of effort into preparing a I18n/Rails workshop here. I'd be absolutely happy to help with it though, of course, if somebody wanted to jump at it.</p> <p>So, if you're going to go to RailsConf Eu next week and are interested in these things be sure to drop me a note and/or catch me at <a href="http://www.bratwurst-on-rails.com/" title="Bratwurst On Rails - A Pre-RailsConf Europe Socializing Event">Bratwurst on Rails</a> (that's the awesome socializing event the evening before the actual RailsConf.)</p> <h2>Changes to the I18n gem library</h2> <p>Since the I18n API and the integration to Rails got merged back to Rails edge we've receive quite a lot of feedback from people trying the I18n API and Simple backend. This resulted in that a few things have been changed. E.g.:</p> <ul> <li>The Simple backend is now a class. This makes it more easy to reuse its features and only overwrite a certain method (like, e.g., for more flexible pluralization).</li> <li>Pluralization data is now expected to be provided as a Hash using keys such as :one, :few, :many, :zero, :other like defined by <a href="http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html" title="Language Plural Rules">CLDR</a>.</li> <li>A method #load_translations has been added that takes a source for translation data (such as yml and rb files) and loads translations from there.</li> <li>The scopes for translations provided by Rails have been cleaned up to be more consistent.</li> <li>The code should work with Ruby 1.9 now.</li> </ul> <h2>Globalize2 under heavy development</h2> <p>A couple of weeks ago <a href="http://www.workingwithrails.com/person/759-joshua-harvey" title="Ruby on Rails developer: Joshua Harvey from Israel, Tel Aviv">Joshua Harvey</a>, <a href="http://workingwithrails.com/person/10731-marko-sepp" title="Ruby on Rails developer: Marko Seppae from Germany, Berlin">Marko Seppae</a> and I have started implementing <a href="http://github.com/joshmh/globalize2" title="joshmh's globalize2 at master &mdash; GitHub">Globalize2</a> which turns out to be an extremely interesting project because with the new I18n foundation it now looks completely different, very slick and nice.</p> <p>As far as we can tell right now Globalize2 will be much more of a toolbox of small tools where you can pick what you need. ActiveRecord translations will be solved unobtrusively. We'll support a good part of <a href="http://www.ietf.org/rfc/rfc4646.txt" title="">RFC4646</a>/<a href="http://www.ietf.org/rfc/rfc4647.txt" title="">47</a> compliance for Locales and use localization data for formats etc. from <a href="http://www.unicode.org/cldr/" title="Unicode CLDR Project">CLDR</a> which is quite a big thing, in my opinion.</p> <h2>We now have a website at rails-i18n.org</h2> <p>You probably already found it because I've been throwing the link around everywhere lately but it might still be worth mentioning that we now have a website at <a href="http://rails-i18n.org" title="Rails I18n">http://rails-i18n.org</a> which is, obviously, dedicated to I18n on and for Ruby on Rails. Right now we're basically collecting resources on the Wiki and I hope to post some news to the blog every once in a while.</p> <p>If you're interested to publish any I18n-related blog posts over there - just let me know.</p> <p>Btw <a href="http://rails-i18n.org" title="Rails I18n">http://rails-i18n.org</a> is, of course, also driven by <a href="http://adva-cms.org/" title="adva cms">adva-cms</a> which is the CMS project I've been working on for the last couple of months. Check it out!</p> 2008-08-28T00:00:00Z tag:svenfuchs.com,2011:finally-ruby-on-rails-gets-internationalized Finally. Ruby on Rails gets internationalized <p>In hindsight we've initially tried to accomplish way to much. Everybody brought their experience and thinking about "good I18n practices" to the table - which proved extremely valuable because it forced everbody to push their own horizon. But it also resulted in something that would have been "just another Rails I18n solution" ... build right into Rails. As such it would not have fully satisfied every one of us. Too heavyweight, too complicated, just too much of everything.</p> <p>So with the beginning of 2008 our work slowed down, good new ideas kept popping up but eventually the project completely stalled and people focussed on other business. Apparently we needed a creative break. </p> <p>Then in May we went back to the drawing board and came up with a fresh approach based on our previous experience. Now all of a sudden we were able to agree on a very slim and minimal implementation of our concepts that covers all of our requirements: it works as a Ruby gem and is suited not only for use in Rails, it adds only minimal load to Rails core and it still implements the common api that will allow I18n solutions to build on.</p> <p>To me this project already proved extremely interesting and educating. Almost all of the ideas that are now implemented have been already present in our discussions last year. But even though we've went through lots of lots of discussions we initially just haven't been able to shave the whole yak fur down to this minimal level.</p> <p>I guess all of us learned a great deal about I18n. In fact some of us (including me) completely changed their minds about what's useful, good practice and common need ... and more importantly: what really can be omitted.</p> <h2>So, what's in the box? And what's not?</h2> <p>First of all, there still won't be a fullblown one-fits-all I18n/L10n solution in Rails. Please, understand that. Rails still won't be able to translate your application to your language as long it's not very similar to US English.</p> <p>Instead, Rails continues to be what it always was: a framework <em>localized to <code>en-US</code></em>. But this time with a twist: it is also internationalized.</p> <p>That means: all hardcoded message strings and logic that we've previously been monkey patching in all of our plugins are now abstracted out of the Rails core code (which we mean by the process of "internationalization") and stored in a central place, accessible through a common interface.</p> <p>We worked hard to stick to the principle of doing "the simplest thing that ever could work" with this. Rails itself won't provide means to localize an application to anything else than <code>en-US</code>. Instead it ships with a solution that gets the job done and walks away.</p> <p>Rails will ship with our I18n Ruby gem which consists of two parts: </p> <ul> <li>The first part is the API itself which is just a Ruby module with a bunch of methods that will be used by Rails and delegate all requests to a backend.</li> <li>The second part is the Simple backend which implements whatever is necessary to re-localize Rails back to <code>en-US</code>.</li> </ul> <p>The Simple backend acutally <em>might</em> also work for simple localization tasks such as translating an error message to another language, but, really, that's a side effect.</p> <h2>So what do we win?</h2> <p>The whole point of the exercise is: the Simple backend can be swapped with a different implementation that supports the same API. Future Rails I18n plugins will do exactly that.</p> <p>E.g. there certainly will be backends that provide different pluralization rules, more powerful means to localize dates and numbers, persist translations in other formats (e.g. Gettext files or in the database). There might be backends that themselves provide some kind of a framework to support pluggable extensions and custom solutions for certain locales etc.</p> <p>So if we still need to rely on plugins for our fullblown L10n support, what do we win then?</p> <p>The short answer: A lot!</p> <p>In the past I18n developers implemented their solutions for the same problems again and again. Our repeatedly reinvented wheels came with different flavours of syntax sugar and different <a href="http://www.bikeshed.com/" title="Why Should I Care What Color the Bikeshed Is?">bikeshed colors</a> of monkey patches. The latter repeatedly broke when Rails moved by a millimeter and plugin developers had to fix their stuff.</p> <p>I'm not saying that this situation was all bad. Actually I really believe that it was necessary for Rails' ecosystem to experiment with all of these solutions. But we've arrived at a point where we can move on.</p> <p>So with this patch applied Rails I18n developers can now build on what was extracted from former experience. Instead of reimplementing things over and over again they can focus on some of the more challenging features like localized inflections, special formattings for strings, dates, numbers etc.</p> <p>We also hope that future solutions will be more exchangeable and pluggable. There hopefully won't be the need to stick to a certain solution anymore just because it's the only one supporting inflections for a certain locale. Or the only one with a strong Gettext backend. Or whatever special feature comes to mind.</p> <p>Of course that's still a long road ahead. But we believe that this step, the merge of the work of the "Rails I18n group", will help a lot on the journey.</p> <h2>Get involved!</h2> <p>If you'd like to join us working on Ruby on Rails's future I18n support, provide feedback or ask questions please do so! You can find our Google Group over at <a href="http://groups.google.com/group/rails-i18n" title="rails-i18n | Google Groups">http://groups.google.com/group/rails-i18n</a>.</p> <p>This solution was developed by the Rails I18n group consisting of <a href="http://railsontherun.com">Matt Aimonetti</a>, <a href="http://www.workingwithrails.com/person/759-joshua-harvey">Joshua Harvey</a>, <a href="http://saimonmoore.net">Saimon Moore</a>, <a href="http://www.arkanis-development.de">Stephan Soller</a> and myself, with the additional work, help and feedback of many other Rails I18n developers including <a href="http://tore.darell.no/">Tore Darell</a>, Chris Eppstein, <a href="http://www.lucaguidi.com">Luca Guidi</a> <a href="http://www.samlown.com">Samuel Lown</a> <a href="http://www.markin.net">Yaroslav Markin</a>, <a href="http://workingwithrails.com/person/5064-joshua-sierles">Joshua Sierles</a>, <a href="http://julik.nl/">Julian 'Julik' Tarkhanov</a> and others.</p> 2008-07-18T00:00:00Z