{"rowid": 153, "title": "JavaScript Internationalisation", "contents": "or: Why Rudolph Is More Than Just a Shiny Nose\n\nDunder sat, glumly staring at the computer screen.\n\n\u201cWhat\u2019s up, Dunder?\u201d asked Rudolph, entering the stable and shaking off the snow from his antlers.\n\n\u201cWell,\u201d Dunder replied, \u201cI\u2019ve just finished coding the new reindeer intranet Santa Claus asked me to do. You know how he likes to appear to be at the cutting edge, talking incessantly about Web 2.0, AJAX, rounded corners; he even spooked Comet recently by talking about him as if he were some pushy web server.\n\n\u201cI\u2019ve managed to keep him happy, whilst also keeping it usable, accessible, and gleaming \u2014 and I\u2019m still on the back row of the sleigh! But anyway, given the elves will be the ones using the site, and they come from all over the world, the site is in multiple languages. Which is great, except when it comes to the preview JavaScript I\u2019ve written for the reindeer order form. Here, have a look\u2026\u201d\n\nAs he said that, he brought up the textileRef:8234272265470b85d91702:linkStartMarker:\u201corder\n form in French\u201d:/examples/javascript-internationalisation/initial.fr.html on the screen. (Same in English).\n\n\u201cLooks good,\u201d said Rudolph.\n\n\u201cBut if I add some items,\u201d said Dunder, \u201cthe preview appears in English, as it\u2019s hard-coded in the JavaScript. I don\u2019t want separate code for each language, as that\u2019s just silly \u2014 I thought about just having if statements, but that doesn\u2019t scale at all\u2026\u201d\n\n\u201cAnd there\u2019s more, you aren\u2019t displaying large numbers in French properly, either,\u201d added Rudolph, who had been playing and looking at part of the source code:\n\nfunction update_text() {\n\tvar hay = getValue('hay');\n\tvar carrots = getValue('carrots');\n\tvar bells = getValue('bells');\n\tvar total = 50 * bells + 30 * hay + 10 * carrots;\n\tvar out = 'You are ordering '\n\t\t+ pretty_num(hay) + ' bushel' + pluralise(hay) + ' of hay, '\n\t\t+ pretty_num(carrots) + ' carrot' + pluralise(carrots)\n\t\t+ ', and ' + pretty_num(bells) + ' shiny bell' + pluralise(bells)\n\t\t+ ', at a total cost of ' + pretty_num(total)\n\t\t+ ' gold pieces. Thank you.';\n\tdocument.getElementById('preview').innerHTML = out;\n}\nfunction pretty_num(n) {\n\tn += '';\n\tvar o = '';\n\tfor (i=n.length; i>3; i-=3) {\n\t\to = ',' + n.slice(i-3, i) + o;\n\t}\n\to = n.slice(0, i) + o;\n\treturn o;\n}\nfunction pluralise(n) {\n\tif (n!=1) return 's';\n\treturn '';\n}\n\n\u201cOh, botheration!\u201d cried Dunder. \u201cThis is just so complicated.\u201d\n\n\u201cIt doesn\u2019t have to be,\u201d said Rudolph, \u201cyou just have to think about things in a slightly different way from what you\u2019re used to. As we\u2019re only a simple example, we won\u2019t be able to cover all possibilities, but for starters, we need some way of providing different information to the script dependent on the language. We\u2019ll create a global i18n object, say, and fill it with the correct language information. The first variable we\u2019ll need will be a thousands separator, and then we can change the pretty_num function to use that instead:\n\nfunction pretty_num(n) {\n\tn += '';\n\tvar o = '';\n\tfor (i=n.length; i>3; i-=3) {\n\t\to = i18n.thousands_sep + n.slice(i-3, i) + o;\n\t}\n\to = n.slice(0, i) + o;\n\treturn o;\n}\n\n\u201cThe i18n object will also contain our translations, which we will access through a function called _() \u2014 that\u2019s just an underscore. Other languages have a function of the same name doing the same thing. It\u2019s very simple:\n\nfunction _(s) {\n\tif (typeof(i18n)!='undefined' && i18n[s]) {\n\t\treturn i18n[s];\n\t}\n\treturn s;\n}\n\n\u201cSo if a translation is available and provided, we\u2019ll use that; otherwise we\u2019ll default to the string provided \u2014 which is helpful if the translation begins to lag behind the site\u2019s text at all, as at least something will be output.\u201d\n\n\u201cGot it,\u201d said Dunder. \u201c _('Hello Dunder') will print the translation of that string, if one exists, \u2018Hello Dunder\u2019 if not.\u201d\n\n\u201cExactly. Moving on, your plural function breaks even in English if we have a word where the plural doesn\u2019t add an s \u2014 like \u2018children\u2019.\u201d\n\n\u201cYou\u2019re right,\u201d said Dunder. \u201cHow did I miss that?\u201d\n\n\u201cNo harm done. Better to provide both singular and plural words to the function and let it decide which to use, performing any translation as well:\n\nfunction pluralise(s, p, n) {\n\tif (n != 1) return _(p);\n\treturn _(s);\n}\n\n\u201cWe\u2019d have to provide different functions for different languages as we employed more elves and got more complicated \u2014 for example, in Polish, the word \u2018file\u2019 pluralises like this: 1 plik, 2-4 pliki, 5-21 plik\u00f3w, 22-24 pliki, 25-31 plik\u00f3w, and so on.\u201d (More information on plural forms)\n\n\u201cGosh!\u201d\n\n\u201cNext, as different languages have different word orders, we must stop using concatenation to construct sentences, as it would be impossible for other languages to fit in; we have to keep coherent strings together. Let\u2019s rewrite your update function, and then go through it:\n\nfunction update_text() {\n\tvar hay = getValue('hay');\n\tvar carrots = getValue('carrots');\n\tvar bells = getValue('bells');\n\tvar total = 50 * bells + 30 * hay + 10 * carrots;\n\thay = sprintf(pluralise('%s bushel of hay', '%s bushels of hay', hay), pretty_num(hay));\n\tcarrots = sprintf(pluralise('%s carrot', '%s carrots', carrots), pretty_num(carrots));\n\tbells = sprintf(pluralise('%s shiny bell', '%s shiny bells', bells), pretty_num(bells));\n\tvar list = sprintf(_('%s, %s, and %s'), hay, carrots, bells);\n\tvar out = sprintf(_('You are ordering %s, at a total cost of %s gold pieces.'),\n\t\tlist, pretty_num(total));\n\tout += ' ';\n\tout += _('Thank you.');\n\tdocument.getElementById('preview').innerHTML = out;\n}\n\n\u201c sprintf is a function in many other languages that, given a format string and some variables, slots the variables into place within the string. JavaScript doesn\u2019t have such a function, so we\u2019ll write our own. Again, keep it simple for now, only integers and strings; I\u2019m sure more complete ones can be found on the internet.\n\nfunction sprintf(s) {\n\tvar bits = s.split('%');\n\tvar out = bits[0];\n\tvar re = /^([ds])(.*)$/;\n\tfor (var i=1; i%s gold pieces.\": '',\n\t\"Thank you.\": ''\n};\n\n\u201cIf you implement this across the intranet, you\u2019ll want to investigate the xgettext program, which can automatically extract all strings that need translating from all sorts of code files into a standard .po file (I think Python mode works best for JavaScript). You can then use a different program to take the translated .po file and automatically create the language-specific JavaScript files for us.\u201d (e.g. German .po file for PledgeBank, mySociety\u2019s .po-.js script, example output)\n\nWith a flourish, Rudolph finished editing. \u201cAnd there we go, localised JavaScript in English, French, or German, all using the same main code.\u201d\n\n\u201cThanks so much, Rudolph!\u201d said Dunder.\n\n\u201cI\u2019m not just a pretty nose!\u201d Rudolph quipped. \u201cOh, and one last thing \u2014 please comment liberally explaining the context of strings you use. Your translator will thank you, probably at the same time as they point out the four hundred places you\u2019ve done something in code that only works in your language and no-one else\u2019s\u2026\u201d\n\nThanks to Tim Morley and Edmund Grimley Evans for the French and German translations respectively.", "year": "2007", "author": "Matthew Somerville", "author_slug": "matthewsomerville", "published": "2007-12-08T00:00:00+00:00", "url": "https://24ways.org/2007/javascript-internationalisation/", "topic": "code"}