15 November 2010

HTML5 Canvas terribly slow drawing lines [not]

Quick heads-up if your canvas application is running awfully slowly, and you're using beginPath(), moveTo(), and/or lineTo().

A simple interpretation of canvas documentation suggests this is a fine way to draw a series of parallel lines (as one might do if one was for example drawing a background grid on one's canvas) ...


  for (var here = start; here < end; here += interval) {
    cx.beginPath();
    cx.moveTo(here, top);
    cx.lineTo(here, bottom);
    cx.stroke();
  }

One would be terribly, terribly wrong! Well, not wrong exactly, but awfully slow ... try this instead


  cx.beginPath();
  for (var here = start; here < end; here += interval) {
    cx.moveTo(here, top);
    cx.lineTo(here, bottom);
  }
  cx.stroke();

The beginPath() and stroke() calls are only needed once each per refresh. My grid drawing dropped from 40ms to 3ms with this change. Not bad ...

19 October 2010

gem install mysql [--should-be-easy [--on-a-mac-anyway]]

So after upgrading to Ruby 1.9.2 and Rails 3 all at once on my shiny new Snow Leopard macbook, (a foolish thing to do! One step at a time! Isolate errors!), after patching my rails app all over the place till rails_upgrade stops whining, and after a gentle reminder to sudo gem install mysql, I get this:


=> Booting WEBrick
=> Rails 3.0.0 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[2010-10-18 23:29:34] INFO  WEBrick 1.3.1
[2010-10-18 23:29:34] INFO  ruby 1.9.2 (2010-08-18) [x86_64-darwin10]
[2010-10-18 23:29:34] INFO  WEBrick::HTTPServer#start: pid=8426 port=3000
dyld: lazy symbol binding failed: Symbol not found: _mysql_init
  Referenced from: /opt/local/lib/ruby1.9/gems/1.9.1/gems/mysql-2.8.1/lib/mysql_api.bundle
  Expected in: flat namespace

dyld: Symbol not found: _mysql_init
  Referenced from: /opt/local/lib/ruby1.9/gems/1.9.1/gems/mysql-2.8.1/lib/mysql_api.bundle
  Expected in: flat namespace

Trace/BPT trap

A search for "lazy symbol binding failed: Symbol not found: _mysql_init" yields this, this, and this ... pointing out that you don't get the wonderful mysql gem without a *little* bit of effort ... but:


$ sudo env ARCHFLAGS="-arch i386" gem install mysql -- --with-mysql-dir=/usr/local/mysql --with-mysql-lib=/usr/local/mysql/lib --with-mysql-include=/usr/local/mysql/include --with-mysql-config=/usr/local/mysql/bin/mysql_config
Building native extensions.  This could take a while...
ERROR:  Error installing mysql:
 ERROR: Failed to build gem native extension.

/opt/local/bin/ruby extconf.rb --with-mysql-dir=/usr/local/mysql --with-mysql-lib=/usr/local/mysql/lib --with-mysql-include=/usr/local/mysql/include --with-mysql-config=/usr/local/mysql/bin/mysql_config
checking for mysql_ssl_set()... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of
necessary libraries and/or headers.  Check the mkmf.log file for more
details.  You may need configuration options.

Provided configuration options:
 --with-opt-dir
 --with-opt-include
 --without-opt-include=${opt-dir}/include
 --with-opt-lib
 --without-opt-lib=${opt-dir}/lib
 --with-make-prog
 --without-make-prog
 --srcdir=.
 --curdir
 --ruby=/opt/local/bin/ruby
 --with-mysql-config
/opt/local/lib/ruby1.9/1.9.1/mkmf.rb:368:in `try_do': The complier failed to generate an executable file. (RuntimeError)
You have to install development tools first.
 from /opt/local/lib/ruby1.9/1.9.1/mkmf.rb:435:in `try_link0'
 from /opt/local/lib/ruby1.9/1.9.1/mkmf.rb:440:in `try_link'
 from /opt/local/lib/ruby1.9/1.9.1/mkmf.rb:552:in `try_func'
 from /opt/local/lib/ruby1.9/1.9.1/mkmf.rb:797:in `block in have_func'
 from /opt/local/lib/ruby1.9/1.9.1/mkmf.rb:693:in `block in checking_for'
 from /opt/local/lib/ruby1.9/1.9.1/mkmf.rb:280:in `block (2 levels) in postpone'
 from /opt/local/lib/ruby1.9/1.9.1/mkmf.rb:254:in `open'
 from /opt/local/lib/ruby1.9/1.9.1/mkmf.rb:280:in `block in postpone'
 from /opt/local/lib/ruby1.9/1.9.1/mkmf.rb:254:in `open'
 from /opt/local/lib/ruby1.9/1.9.1/mkmf.rb:276:in `postpone'
 from /opt/local/lib/ruby1.9/1.9.1/mkmf.rb:692:in `checking_for'
 from /opt/local/lib/ruby1.9/1.9.1/mkmf.rb:796:in `have_func'
 from extconf.rb:50:in `
' Gem files will remain installed in /opt/local/lib/ruby1.9/gems/1.9.1/gems/mysql-2.8.1 for inspection. Results logged to /opt/local/lib/ruby1.9/gems/1.9.1/gems/mysql-2.8.1/ext/mysql_api/gem_make.out

"You have to install development tools first." ... thanks dudes ... but sudo port instal mysql-devel tells me Error: Port mysql_devel not found. What are these mysterious development tools?

Someone recommends copying the output of mysql_config --cflags to the ARCHFLAGS env var, but still no go:


$ mysql_config
Usage: /usr/local/mysql/bin/mysql_config [OPTIONS]
Options:
        --cflags         [-I/usr/local/mysql/include -Os -arch i386 -fno-common]
        --include        [-I/usr/local/mysql/include]
        --libs           [-L/usr/local/mysql/lib -lmysqlclient -lz -lm]
        --libs_r         [-L/usr/local/mysql/lib -lmysqlclient_r -lz -lm]
        --socket         [/tmp/mysql.sock]
        --port           [3306]
        --version        [5.0.37]
        --libmysqld-libs [-L/usr/local/mysql/lib -lmysqld -lz -lm]

$ sudo env ARCHFLAGS="-Os -arch i386 -fno-common" gem install mysql -- --with-mysql-dir=/usr/local/mysql --with-mysql-lib=/usr/local/mysql/lib --with-mysql-include=/usr/local/mysql/include --with-mysql-config=/usr/local/mysql/bin/mysql_config
all the same errors, all over again

The usual drill, I run these commands a few times to make sure they don't change their minds if I insist politely ... no bacon. But what's the deal with mysql_config5? ... it turns out that it's *not* the same as mysql_config ... it's not the same thing at all, and it has completely different output:


$ mysql_config5
Usage: /opt/local/bin/mysql_config5 [OPTIONS]
Options:
        --cflags         [-I/opt/local/include/mysql5/mysql  -pipe -fPIC    -D_P1003_1B_VISIBLE -DSIGNAL_WITH_VIO_CLOSE -DSIGNALS_DONT_BREAK_READ -DIGNORE_SIGHUP_SIGQUIT  -DDONT_DECLARE_CXA_PURE_VIRTUAL]
        --include        [-I/opt/local/include/mysql5/mysql]
        --libs           [-L/opt/local/lib   -L/opt/local/lib/mysql5/mysql -lmysqlclient -L/opt/local/lib -lz -lm  -L/opt/local/lib/ -lssl -lcrypto]
        --libs_r         [-L/opt/local/lib   -L/opt/local/lib/mysql5/mysql -lmysqlclient_r -L/opt/local/lib -lz -lm   -L/opt/local/lib/ -lssl -lcrypto]
        --plugindir      [/opt/local/lib/mysql5/mysql/plugin]
        --socket         [/opt/local/var/run/mysql5/mysqld.sock]
        --port           [0]
        --version        [5.1.51]
        --libmysqld-libs [-L/opt/local/lib   -L/opt/local/lib/mysql5/mysql -lmysqld -ldl  -L/opt/local/lib -lz -lm     -L/opt/local/lib/ -lssl -lcrypto]

This looks rather serious. Let's plug these values into our gem install command and see what happens:


$ sudo env ARCHFLAGS="-I/opt/local/include/mysql5/mysql  -pipe -fPIC    -D_P1003_1B_VISIBLE -DSIGNAL_WITH_VIO_CLOSE -DSIGNALS_DONT_BREAK_READ -DIGNORE_SIGHUP_SIGQUIT  -DDONT_DECLARE_CXA_PURE_VIRTUAL" gem install mysql -- --with-mysql-dir=/usr/local/mysql --with-mysql-lib=/opt/local/lib/mysql5/mysql --with-mysql-include=/opt/local/include/mysql5/mysql --with-mysql-config=/opt/local/bin/mysql_config5
Building native extensions.  This could take a while...
Successfully installed mysql-2.8.1
1 gem installed
Installing ri documentation for mysql-2.8.1...
Installing RDoc documentation for mysql-2.8.1...

Wow! Bingo! Bring on the beer!

Well there you go I hope this helps, or at least gives you some ideas, if you're stuck with Symbol not found: _mysql_init or extconf.rb failed when you can't get your mysql gem to install.

29 September 2010

Fun with regex: shrinking indentation

Sometimes you get source code from people who believe in 4 spaces. Or 8, imagine! Or tabs ... well that's just *so* 20th century ...


    function($) {
        $("#blah").toto(function(event) {
            $(this).click(function(event2) {
               $(this).goes(WAY.off(2, the("right")));
            });
        });
    };

Too much indentation! 2 is enough! Let's suppose that for whatever reason, you can't or won't use your text editor's re-indent or reformat function, or you just really dig regular expressions ... here's what to do: replace ^( +)\1 with $1.

^( +)\1 means "any nonzero-length sequence of spaces at the start of the line, followed by the same sequence of spaces. The \1 in the pattern, and the $1 in the replacement, are both back-references to the initial sequence of spaces. Result: indentation halved.


  function($) {
    $("#blah").toto(function(event) {
      $(this).click(function(event2) {
        $(this).goes(WAY.off(2, the("right"))); // Not.
      });
    });
  };

There you go. Readable code.

Don't tell anyone I said this, but if you want to do the opposite (increase indentation), replace ^( +) with $1$1 ...

28 September 2010

Stuck for an idea?

If you're a developer with an entrepreneurial bent and don't have access to a real flesh-and-blood business/marketing type, don't worry, these kinds of resources are becoming progressively more and more automated. Here are two to get you started:

The Startup Idea Generator will give you great, buzzword-compliant mission statements such as "Synthesize wireless bandwidth consistently through peer-to-peer AJAX-tagging communities," "Harness robust users in conjunction with crowdsourced android tagging sites," "Implement global infrastructures using venture-backed social media-tracking relationships." You can't possibly lose!

Wait, what does your startup do? will help you hone your elevator pitch and optimize your marketing strategy by leveraging your next-generation wireless browser investments. "So, Basically, it's like a ..."

  • Database Abstraction Layer For The Army!
  • Eco-Friendly Marketplace For Social Outcasts!
  • iPhone App For Ex-Girlfriends!
  • Match.com for Pets!

But don't take my word for it, go see for yourself. It's like an optimizer for your spare time!

Thanks @peignoir for sharing itsthisforthat

15 September 2010

Start In Paris #3

Last night was the third Start In Paris. At La Cantine, as usual, 5 startups presented themselves, so for those of you who couldn't make it, here you are:

Wizme

This is a way of seeking and making recommendations from/to your personal social network. The goal is to make recommendations relevant because they're from people you trust, instead of from random strangers on the internet whose opinions are possibly biased in one direction or another.

The plan for now is to rely on networks you've already established in facebook and twitter. Here's a challenge though: if someone asks you what you think of your new telephone / jeans / dentist over beer, you're generally delighted to expound wisely and wittily on the subject. If you get the same request through facebook, won't it feel a little bit like work? Or am I just old-fashioned?

The concept could be très très cool if it works. No amount of marketing will sell me the latest hot Apple product or hollywood movie if the people I know and respect are telling me it sucks. If this kind of site can successfully undermine the mass marketers, we could be moving to a better kind of world. Of at least, a better kind of France - the site is only in French so far ...

Hop-cube

Hop-cube aims to exploit the growing market of environmentally- and ecologically-aware consumers. Their three services are (1) The "Hop-Badge" that you can show on your site to illustrate how eco-friendly your product is; (2) hop-score.com, which lists zillions of products along with their environmental-impact information; and (3) Special marketing tools based on sustainable development concepts.

Prices and affiliate links to sellers are available immediately on a hop-score.com product page; environmental details require an extra click. Of course they have to make their money somehow, but the way information is prioritised here is surprising given their declared mission. The environmental details page shows the usual stuff - energy use, greenhouse gas emissions, and also information about the manufacturer such as their global gas emissions, water use, ISO14001 certification.

It could be interesting to push this further to understand, for example, how much energy was used to manufacture this product, how much gas emissions, what kind of resources are mined for raw materials for this product, how many rivers are polluted, hectares of forest destroyed for this product, and how many Chinese factory slaves died for this beautiful sleek slender unibody laptop I so enjoy typing on while its mercury-free battery warms my thighs?

On "Sustainable Development Tools", from their site (http://www.hop-cube.com/services/outils-marketing-durables.html, roughly translated), "[our] simulators take into account each of your visitor's specific behaviour and data" ... "besides, the simulators give you a mine of information about your visitors". Well, well. Environment++, maybe; privacy--- ...

In any case, I'll be sure to take a peek the next time I'm buying a great lump of electricity-sucking metal for my home. And delete all my cookies afterwards :)

Dress-Me

Your problem is, you have no sense of style. And you have a major romantic dinner coming up at the end of the week. Or an interview. Or something ... something that causes you, even involuntarily, to think about what you will wear. You have the clothes, even though the few decent ones are musty from disuse. But your sense of style, your finger on the pulse of Parisien fashion, where is it? It's not where you are, and that's your problem.

Dress-Me is a whole platform for whatever you might want to do with your clothes. Buy/sell/exchange/borrow; seek and offer fashion advice; and most useful for someone like me: hire a personal shopper who can expertly tell me what I ought to wear, because, honestly, I haven't a clue.

I can see Dress-Me grow into an awesome community of clothing and fashion geeks, the kind of place where ideas germinate and influence the world outside. Not so sure about popular appeal to jeans-and-tshirt types. But it looks like a lot of fun.

Super Marmite

If you're hungry but can't/won't cook, and you're fed up with the local macjunk, and you simply *know* your neighbours are cooking something delicious, Super-Marmite is where you need to go!

This is a social network for foodies, whatever side of the kitchen you're on. Buy meals, sell meals, meet your neighbours, and eat authentic, genuine, and (hopefully) wholesome food. Especially if your neighbours are Korean, or Japanese, or something like that. Or Senegalese, or Moroccan, or Mexican. Or Italian. You get the idea.

They're planning for two kinds of cook: the casual, occasional kind who's in it for society and fun; and the professional kind, who sees a way to make substantial revenue from their kitchen.

They're still in beta, but I'm sure that won't stop you, go request an invitation

scrumers

If you're unfortunate enough to know what scrum is (hint: nothing to do with rugby), this site deserves a look. One of its features is a "Rich User Interface", and the ability to move tasks around just like post-it notes on a wall!

You might be cynical and argue that you can do that with post-it notes and a wall, but since when did your wall automatically generate burn-down charts, huh? And what do you do when the wall is in another continent? Answer me that you cynical so-and-so.

Comes with obligatory i-phone app. And, unlike every other startup presenting this evening, the folks at scrumers seem to think that somewhere in the world, hiding in a corner on the other side of the planet perhaps, there might be some customers who speak Ze English!! Radical stuff here. Here we are, in the middle of Paris, and the site's entirely in English. Do they think the internet is global or something?

06 September 2010

Discussion

I met a software project manager once who argued that a team of good software developers would be a disaster:

"they would spend the whole time arguing with one another."

Ouch. If they spend whe whole time arguing with one another instead of delivering your project, they're not good software developers. They're crap, in fact. How interesting that this particular manager was unable even to imagine the qualities of a team of good developers, having never experienced one, perhaps!

There's a time to lead, and a time to follow, and an expert (in any profession) knows which time it is. If you find everyone else is talking to much, remember that it might be better to do the wrong thing than to do nothing. It will surely be a learning experience for somebody.

Discuss.

31 August 2010

Mindful Reading

Sometimes, while Bob is reading, a word, a sentence, or an idea on the page will trigger a memory, or a fantasy, or another idea, and quickly Bob's mind is lost in a chain-reaction of these distractions. This might be a minor irritation, but the text-tracking portion of Bob's brain keeps running, on auto-pilot, following words, sentences, paragraphs, even whole pages, while the rest of his brain is away on holiday.

All of a sudden Bob remembers that he was supposed to be reading, it's why he's there after all, and he wanted to do it. But now it takes energy to read backwards and identify the point where his consciousness took the wrong turn, off the page. Then it takes even more energy to re-read the subsequent text, text that his tracker thinks it has already read.

Poor Bob. Lucky for him, he's beginning to practise mindfulness, having been inspired by Mindfulness in Plain English, a short book describing the technique. Mindfulness is the habit of being aware of your immediate environment at all times, including most importantly being aware of what's going on inside your head.

Bob's able to catch himself sooner when a distraction arises, and is able to consciously decide whether to return to the page, or stop reading and pursue the distraction instead (and maybe even write a blog entry about it). Now, he's beginning to notice that he gets more read in less time, and the experience of reading is more satisfactory.

I'll let you know how it goes for him.

27 August 2010

Active Record objects backed by Database Views

"Views" are a powerful feature of relational databases - a view presents the result of a query as if it was a simple table. You can select from a view or join it in another query. You can even use a view in the definition of another view. Using a view, you can hide some nasty, complicated, but useful SQL behind what appears to be a plain old table.

Here's a simple example, using mysql:

CREATE TABLE users (id integer primary key,
                    username varchar(16) not null,
                    crypted_password varchar(128) not null,
                    etc etc);

CREATE TABLE posts (id integer primary key,
                    user_id integer not null,
                    content text);

Suppose you like to keep track of posts-per-user, but you want users.username, not users.id in your results, because you are not the database and you like humanly recognisable names.

SELECT select u.username AS username, 
       (select count(*) from posts p where p.user_id = u.id) as total 
  from users u;

Depending on your specific needs, you might tack on "order by total desc limit 10" or "where u.username = 'me_again_999". It's an awful lot of stuff to type each time. Try this instead:

CREATE VIEW post_count AS
     select u.username AS username, 
            (select count(*) from posts p where p.user_id = u.id) as total
       from users u;

And now:

mysql> select * from post_count where username = 'me_again_999';
+--------------+-------+
| username     | total |
+--------------+-------+
| me_again_999 |    16 |
+--------------+-------+


mysql> select * from post_count order by total desc limit 3;
+--------------+-------+
| username     | total |
+--------------+-------+
| verbose      |   256 |
| eloquacious  |    64 |
| me_again_999 |    16 |
+--------------+-------+

Easier, no?

Now, the cool thing is that this works no matter how ugly, incomprehensible, or complicated your query is. And the über-cool thing is: you can create a Rails model on top of your view, ActiveRecord doesn't care that it's not a table.

We need a view that contains user_id instead of username so that active_record can make the join:

CREATE VIEW `post_count` AS 
      select u.id AS user_id, 
             (select count(*) from posts p where p.user_id = u.id) as total
        from users u;

class PostCount < ActiveRecord::Base
  belongs_to :user
end

class User < ActiveRecord::Base
  has_one :post_count
end

And now:

User.all.each { |u| puts "#{u.username} #{u.post_count.total}"}
verbose 256
eloquacious 64
me_again_999 16

So is that über-cool or what?

25 June 2010

Go to prison for sharing music with your friends

According to a leaked document obtained by La Quadrature du Net, our delightful EU leaders want to criminalise not-for-profit music sharing, as part of ongoing ACTA negotiations. In other words, you do some free marketing on behalf of Big Content Industry, they put you in jail. Is this the kind of world you want to live in?

Thanks to boingboing for the link.

18 June 2010

"PassengerEnabled off" not working

We needed to disable a Rails site for a little bit and figured PassengerEnabled off and a little bit of apache mod_rewrite would do the trick in the least painful way ... but Passenger refused to turn itself off and kept serving our (slightly broken) rails app!

After some hours of bewildered searching and starting to doubt our sanity, we found the culprit: PassengerHighPerformance on - cut that line and PassengerEnabled off works as advertised.

This is not even slightly Phusion's fault, btw - PassengerHighPerformance is clearly marked as experimental and flaky in their excellent docs. What we need to do, in the general case, is figure out a foolproof way of advertising the presence of a potentially buggy option in our config files (whatever the software product is) so it's easy for any team member to eliminate likely causes of incorrect behaviour when the bug-hunt is on.

This article is a start, I guess :)

11 February 2010

ActionMailer and Multiple SMTP Accounts (useful with gmail)

ActionMailer works with only one smtp account; a single google apps account is limited to 500 (or 2000) mails per day; my app legitimately needs more. Besides, I'd rather notifications come from an appropriately named account rather than a generic "no-reply@example.com" address. Here's what I came up with. Firstly, config/smtp.yml describes my various accounts - I settled on one per mailer class. Secondly, a patch to ActionMailer::Base enables switching smtp accounts based on the mailer class.

Here's an example with four mailers: an account mailer, an exception notifier (works with the lovely ExceptionNotifier plugin), a "share this" mailer so your site can be all viral and stuff, and a prize mailer for the good news.

config/smtp.yml

defaults: &defaults
  address:        smtp.gmail.com
  port:           587
  domain:         example.com
  authentication: !ruby/sym plain

account_mailer:
  <<: *defaults
  user_name:      accounts@example.com
  password:       "pw1"

prize_mailer:
  <<: *defaults
  user_name:      winner@example.com
  password:       "pw2"

exception_notifier:
  <<: *defaults
  user_name:      dev_team_obviously_sucks@example.com
  password:       "pw3"

share_this_mailer:
  <<: *defaults
  user_name:      share_this@example.com
  password:       "pw4"

ActionMailer::Base patch

require 'smtp_tls'

module ActionMailer
  class Base
    cattr_accessor :smtp_config

    self.smtp_config = YAML::load(File.open("#{RAILS_ROOT}/config/smtp.yml"))

    def smtp_settings
      smtp_config[mailer_name].symbolize_keys
    end
  end
end

I put this in config/initializers/action_mailer.rb. And that's it! No changes required to your mailers or email templates or anything else in your application. As you can see, the patch merely overrides ActionMailer's smtp_settings class method and replaces it with an instance method that decides at sending-time which smtp configuration to use.

We needed to do this because sendmail was failing erratically - some users weren't getting any mail from our app at all - presumably due to hypersensitive spam filters somewhere on the chain, maybe related to my not understanding how SPF records are supposed to work.

You could easily fancify this to switch SMTP config based on the time of day, or based on your user's locale (so you can use a nicely localised "from" address - Google overrides the "from" address sent by ActionMailer), or even based on whether the Moon is in Scorpio if you cared. Just replace the call to mailer_name with a call to your config-switching method.

I understand that rails 3 is beautifuller in many ways including the way ActionMailer works so this might well be obsolete in a few months except for you suckers working on legacy systems. I hope this helps, let me know one way or the other.

07 February 2010

it wasn't git actually

Ouch! Panic!

$ git push origin master
fatal: unable to fork

and later,

$ git push origin master
fatal: git-pack-objects failed (Resource temporarily unavailable)

But it had nothing to do with git or a corrupted repository as google seemed to be trying to suggest; it was rather my trusty mac running out of something. The solution: shut down itunes and try again.

30 January 2010

Upgrading Gutsy to Hardy

My slicehost slice was running Gutsy (Ubuntu 7.10), but when I switched my projects from my personal svn server to github, gutsy only had an early version of git that doesn't support submodule ... the only way to upgrade git was to upgrade my ubuntu.

After a quick mysql backup, these are the instructions that worked:

sudo vi /etc/apt/sources.list
# [ replace each "gutsy" with "hardy" ]
sudo apt-get update
sudo apt-get dist-upgrade

I found this solution on ubuntugeek.com. It worked first time, like a charm. Congratulations ubuntu team ... linux has come a long, long way

29 January 2010

Back up your mysql database with mysqldump

I don't use this often so I end up googling it every time I need it.

mysqldump -u root -p database_name > sql_dump_file

Now I know exactly where to find it and I don't have to scan a whole article just to get the syntax.

Change "root" to the user you normally use; you an also specify -ppassword (no space between -p and the password) so you don't have to enter the password interactively ... security issues etc but if you're running this from a script I'm not sure what the alternative is.

28 January 2010

What kind of World do You want to inhabit?

Whatever you think of Stallman, go have a look at this quick dystopia, and imagine what kind of world you would like to live in. Then head over to boingboing and learn about ACTA. I preferred the days when Russia and China were the bad guys, and they were far away.

27 January 2010

It should have no missing translations!

I'm a big fan of rspec and of rails' I18n, and I don't like having to study yml translation files over and over to make sure every key has a translation in every language; so I wrote it_should_have_no_missing_translations to test my templates for missing translations.

Previously, I needed this for every template:

  it "should have no missing translations in fr" do
    I18n.locale = "fr"
    do_render
    response.should_not have_tag("span.translation_missing")
  end

  it "should have no missing translations in en" do
    I18n.locale = "en"
    do_render
    response.should_not have_tag("span.translation_missing")
  end

Where do_render knows how to render the template I'm testing. If you're like me, and I presume you are, you're thinking the duplication up there is a bit annoying and someone should do something about it. Well, here you go:

  it_should_have_no_missing_translations

You like? Obviously, it_should_have_no_missing_translations needs a bit of context, like an implementation of do_render, and any other setup you need. Here's the implementation, under the WTFPL. Copy it into your specs or make a helper out of it that you include in your spec, or publish it in a gem and become famous.

  def it_should_have_no_missing_translations
    INSTALLED_LANGUAGES.each do |lang|
      it "should not have translations missing in #{lang}" do
        I18n.locale = lang
        do_render
        response.should_not have_missing_translations
      end
    end
  end

have_missing_translations is defined thus:

  INSTALLED_LANGUAGES = [:en, :fr] unless defined?(INSTALLED_LANGUAGES)

  class TranslationsMissing
    def initialize(scope)
      @scope = scope
    end

    def matches? response
      if response.is_a? String
        root_node = HTML::Document.new(response, false, false).root
      else
        root_node = HTML::Document.new(response.body, false, false).root
      end
      m = @scope.css_select root_node, ".translation_missing"
      @missing_translations = []
      m.each do |mt|
        @missing_translations << mt.children.first
      end
      m.size > 0
    end

    def failure_message
      "expected that response would contain a translation_missing element, but it didn't"
    end

    def negative_failure_message
      "expected that response would contain no missing translations, but it contained these \n#{@missing_translations.join("\n")}"
    end
  end

  def have_missing_translations
    TranslationsMissing.new self
  end