WordCamp Rhode Island 2015 : Introduction to i18n / l10n

B988db88ba317c0cfc2ca00b6275e99d?s=47 Dave McHale
September 26, 2015

WordCamp Rhode Island 2015 : Introduction to i18n / l10n

Internationalization and Localization for WordPress developers. An introduction to why i18n is important, and how to get started making your plugin or theme translation-ready


Dave McHale

September 26, 2015


  1. 2.

    i n t e r n a t i o

    n a l i z a t i o n 1 + 18 +1 i18n l o c a l i z a t i o n 1 + 10 + 1 l10n
  2. 5.

    Actually… no • In the 2014 State of the Word,

    Matt Mullenweg reported that WordPress had more non-English downloads than English downloads in that year - http://ma.tt/2014/10/sotw-2014/ • That is a BIG deal. Non-English growing quickly! • 7.2 Million non-English downloads versus 6.5 Million English
  3. 6.

    Why do we care • 71% of all current WordPress.com

    sites are English, 29% are non-English https://wordpress.com/activity/ • 74.4% of WordPress 4.3 downloads are US English, and 25.6% in non-English – Additional language packs are not counted in these numbers
  4. 7.

    Why do we care • WordPress is currently running over

    24% of the internet • If we use the WordPress.com usage percentages, “only” 29% of that market share == 6.96% of the internet is running a non-English WordPress install
  5. 8.

    Why i18n matters for growth • Social Media sites like

    Facebook, Twitter and others experienced 30% growth in their first year after localizing • What's helped? Numerous factors, but rising global usage is an "easy" place to gain market share • Continued improvements to i18n process only continue to make the platform more accessible https://speakerdeck.com/petya/wordpress-is- growing-globally-are-you
  6. 10.

    Introduction to __() • The double-underscore function • Most common

    • Most basic, but fits most needs • Based on the _() (single underscore) gettext function, but specifically for WordPress in order to load translation files “In computing, gettext is an internationalization and localization (i18n) system commonly used for writing multilingual programs on Unix-like computer operating systems. The most commonly used implementation of gettext is GNU gettext, released by the GNU Project in 1995.” - https://en.wikipedia.org/wiki/Gettext
  7. 11.
  8. 12.

    Using __() echo __('Hack the planet!', 'my-text-domain'); ‘my-text-domain’ is the

    text domain which you have defined for your plugin or theme /* Plugin Name: Hello Dolly Plugin URI: http://wordpress.org/plugins/hello-dolly/ Author: Matt Mullenweg … Text Domain: hello-dolly */
  9. 13.

    Never translate variables echo __("Hack the $noun!", 'my-text-domain'); echo __($verb

    . ' the planet!', 'my-text-domain'); echo __('Hack the planet!', $my_text_domain); • Variables will not get parsed properly inside double quotes • Using string concatenation will not help • Translation tools cannot interpret text domain dynamically
  10. 14.

    Translate full phrases, NOT this echo __('Hack the', 'my-text-domain') .

    $noun . __('! Hack the planet!', 'my-text-domain'); • Translators lose reference points when you break apart your strings in this way • We need to use a different method to use variables in an intelligent manner
  11. 15.

    Creating the .pot file • When our code is prepared

    for translation, we will run it through a program that generates a .pot file • .pot = portable object template • File contains all translatable strings • Can be generated… – Through the WP repository – Using Loco Translate – Via command-line, using the WP i18n tools – Grunt – GlotPress https://codex.wordpress.org/I18n_for_WordPress_Developers https://wordpress.org/plugins/loco-translate/ # Loco Gettext template #, fuzzy msgid "" msgstr "" "Project-Id-Version: i18n test\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: Thu Sep 24 2015 18:38:43 GMT-0400 (Eastern Daylight Time)\n" "POT-Revision-Date: Thu Sep 24 2015 18:38:51 GMT-0400 (Eastern Daylight Time)\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: \n" "Language-Team: \n" "Language: \n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-Basepath: .\n" "X-Poedit-SearchPath-0: .\n" "X-Poedit-KeywordsList: _:1;gettext:1;dgettext:2;ngettext:1,2;dngettext:2,3;" "__:1;_e:1;_c:1;_n:1,2;_n_noop:1,2;_nc:1,2;__ngettext:1,2;__ngette xt_noop:1,2;" "_x:1,2c;_ex:1,2c;_nx:1,2,4c;_nx_noop:1,2,3c;_n_js:1,2;_nx_js:1,2,3 c;" "esc_attr__:1;esc_html__:1;esc_attr_e:1;esc_html_e:1;esc_attr_x:1, 2c;" "esc_html_x:1,2c;comments_number_link:2,3;t:1;st:1;trans:1;transC hoice:1,2\n" "X-Generator: Loco - https://localise.biz/"
  12. 16.

    Translating the .pot file • Someone (maybe even you!) will

    translate the strings in your code into another language • Once translation is complete, two files are created – .po and .mo files • These files are named for the language scheme they apply to – es_ES.po and es_ES.mo for Spanish – pl_PL.po and pl_PL.mo for Polish – etc • Those files are added to the /languages subfolder of your code
  13. 17.

    .pot file results echo __('Hack the planet!', 'my-text-domain'); #: /i18n-test.php:14

    /i18n-test.php:17 msgid "Hack the planet!" msgstr "" echo __("Hack the $noun!", 'my-text-domain'); echo __($verb . ' the planet!', 'my-text-domain'); • No results. The parser skips these entirely
  14. 18.

    .pot file results echo __('Hack the', 'my-text-domain') . $noun .

    __('! Hack the planet!', 'my-text-domain'); #: /i18n-test.php:18 msgid "Hack the" msgstr "" #: /i18n-test.php:18 msgid "! Hack the planet!" msgstr ""
  15. 20.

    Enter sprintf() • While not technically a translation function, sprintf()

    gives us the ability to use variables in our translation strings echo sprintf( __(‘Your mom goes to %s', 'my-text-domain'), $noun ); • The inner part of the function leaves us only translating ‘Your mom goes to %s’ (or its foreign-language equivalent) • sprintf() will replace the %s with $noun • %s is the symbol for strings, but there are a number of type specifiers that can be used - http://php.net/manual/en/function.sprintf.php
  16. 21.

    sprintf() with multiple variables • What if we have more

    than one variable? echo sprintf( __('Your %s goes to %s', 'my-text-domain'), $relative_title, $noun ); • Percent variables are used as placeholders in the string we want to be translated • Their order in the original string will match the order of our variable parameters passed to sprintf()
  17. 22.

    .pot file interpretations of sprint() echo sprintf( __('Your mom goes

    to %s', 'my-text-domain'), $noun ); #: /i18n-test.php:14 #, php-format msgid "Your mom goes to %s" msgstr "“ echo sprintf( __('Your %s goes to %s', 'my-text-domain'), $relative_title, $noun ); #: /i18n-test.php:15 #, php-format msgid "Your %s goes to %s" msgstr ""
  18. 23.

    sprintf() • What if the order of your variables needs

    to change? • In the translation, the noun may need to precede the verb echo sprintf( __('Your %1$s goes to %2$s', 'my-text-domain'), $relative_title, $noun ); • This is another really good reason not to break up your phrases into multiple gettext functions
  19. 24.

    sprintf() • By using the N$ notation within the placeholder

    variables, we can identify which variable goes with which placeholder. • %2$s will always match to $noun, no matter where %2$s goes in the translation string echo sprintf( __(‘Your %1$s goes to %2$s! Because %2$s is a bad thing!', 'my-text-domain'), $relative_title, $noun );
  20. 25.

    .pot file interpretations of sprintf() echo sprintf( __(‘Your %1$s goes

    to %2$s', 'my-text-domain'), $relative_title, $noun ); #: /i18n-test.php:16 #, php-format msgid "Your %1$s goes to %2$s" msgstr "" echo sprintf( __(‘Your %1$s goes to %2$s! Because %2$s is a bad thing!', 'my-text-domain'), $relative_title, $noun ); #: /i18n-test.php:17 #, php-format msgid "Your %1$s goes to %2$s! Because %2$s is a bad thing!" msgstr ""
  21. 27.

    One last look at __() and sprintf() echo __('Hello. My

    name is Inigo Montoya.', 'my-text-domain'); echo sprintf( __('Hello. My name is %1$s.', 'my-text-domain'), $my_name ); echo sprintf( __('%2$s. My name is %1$s.', 'my-text-domain'), $my_name, $greeting_text );
  22. 28.

    Using _e() and printf() to echo • As a shortcut,

    you can combine “echo” and “__()” into the _e() function _e(‘Hello. My name is Inigo Montoya.', 'my-text-domain'); • While sprintf() returns a string, which can be used however you like, you can alternatively call printf() to just echo the result of whatever is inside the function printf( __('You killed my %s. Prepare to die.', 'my-text-domain'), $relative );
  23. 30.

    Dealing with multiples for numeric values echo sprintf( _n('I want

    my %d dollar!', 'I want my %d dollars!', $int_dollars_owed, 'my-text-domain'), $int_dollars_owed ); • _n() takes 4 parameters – Singular string, Plural string, Numeric value, and Text Domain • In this example, $int_dollars_owed is first used inside of _n(), then used for replacement as part of sprintf() • %d is our placeholder in this example because we’re using digits
  24. 31.

    Custom conditions for numeric values • The codex recommends writing

    your own explicit conditionals for special cases if ($int_dollars_owed === 0) { echo __(‘You don't owe me any money!’); } else { echo sprintf( _n('I want my %d dollar!', 'I want my %d dollars!', $int_dollars_owed, 'my-text-domain'), $int_dollars_owed ); } https://codex.wordpress.org/I18n_for_WordPress_Developers
  25. 32.

    .pot interpretations of _n() echo sprintf( _n('I want my %d

    dollar!', 'I want my %d dollars!', $int_dollars_owed, 'my-text- domain'), $int_dollars_owed ); #: /i18n-test.php:14 /i18n-test.php:20 #, php-format msgid "I want my %d dollar!" msgid_plural "I want my %d dollars!" msgstr[0] "" msgstr[1] "" if ($int_dollars_owed === 0) { echo __(‘You don\'t owe me any money!’); } else { echo sprintf( _n('I want my %d dollar!', 'I want my %d dollars!', $int_dollars_owed, 'my- text-domain'), $int_dollars_owed ); } #: /i18n-test.php:18 msgid "You don't owe me any money!" msgstr "“ AND
  26. 34.

    Dealing with homonyms and homographs • Homonym: same spelling, same

    pronunciation – “A bear can bear very cold temperatures” • Homograph: same spelling, different pronunciation – “The violin player held his bow while taking a bow” • In text, it can be difficult to convey our meaning with our words • #polyglots, “ […] in Jetpack: “Set the primary account holder”. Does anyone know if it is the holder of the primary account, or is it the primary holder of the account?”
  27. 35.

    Providing context with _x() echo _x( 'I like to see

    girls of that... caliber', 'by caliber I mean the high quality of their characters', 'my-text-domain' ); • _x() takes the string to be translated, the context for the person doing the translation, and the text domain Dr. Evil: I like to see girls of that... caliber. [pause] Dr. Evil: By "caliber," of course, I refer to both the size of their gun barrels and the high quality of their characters... Two meanings... caliber... it's a homonym... Forget it. ” – Doctor Evil http://www.imdb.com/character/ch0026630/quotes
  28. 36.

    .pot interpretation of _x() echo _x( 'I like to see

    girls of that... caliber', 'by caliber I mean the high quality of their characters', 'my-text-domain' ); #: /i18n-test.php:15 msgctxt "by caliber I mean the high quality of their characters" msgid "I like to see girls of that... caliber" msgstr ""
  29. 37.

    Leaving notes for translators • Sometimes you may have a

    need for adding a specific note for translators, which could be different than simply providing context of a single word or phrase using _n() • You can do this with the /* translators: comment notation • Your note will be added to the pot file for translators as long as it is the last comment before the gettext function call /* translators: draft saved date format, see http://php.net/date */ $draft_saved_date_format = __( 'g:i:s a' );
  30. 38.

    .pot interpretation of /* translators: /* translators: draft saved date

    format, see http://php.net/date */ $draft_saved_date_format = __( 'g:i:s a' ); #. translators: draft saved date format, see http://php.net/date #: /i18n-test.php:15 /i18n-test.php:72 msgid "g:i:s a" msgstr ""
  31. 39.

    Final notes, for your i18n to DO something • Text

    Domain MUST match your plugin/theme slug • Plugins add_action('init', 'my_plugin_init'); function my_plugin_init() { load_plugin_textdomain('my-text-domain', false, dirname(plugin_basename(__FILE__)) . '/languages'); } • Themes add_action('after_setup_theme', 'my_theme_setup'); function my_theme_setup() { load_theme_textdomain('my-text-domain', get_template_directory() . '/languages'); }
  32. 40.

    Summary • __(), _e() • sprintf(), printf() • _n() •

    _x() • /* translators: • What .pot, .po, and .mo files are and how they work together • Not covered: – Escaping content, more _n() love, dates, number formatting, and more!
  33. 41.

    Final Warnings, “Gotchas”, and Misc • Avoid – Newline characters

    – HTML in translations – URLs in translations • Unless it’s translatable – Empty strings • Notes – Many sentences at once is okay, but don’t translate War and Peace. Use paragraphs, at least. • More Good Stuff – Many exciting changes are coming to GlotPress soon – http://translate.wordpress.org – Localized versions of the plugin & theme repositories exist – Language Packs in the repo! – Streamlining the author/translator relationship right in the repository, similar to contributors (proposal)
  34. 42.

    Additional Resources • WP Links – https://codex.wordpress.org/I18n_for_WordPres s_Developers – https://developer.wordpress.org/plugins/internati

    onalization/ – https://developer.wordpress.org/themes/function ality/localization/ – https://make.wordpress.org/polyglots/handbook/ • Otto’s Blog - http://ottopress.com/tag/i18n/ • Slack – #meta-i18n and #polyglots • WP Repository – https://wordpress.org/plugins/ – https://wordpress.org/themes/ • Google! Contact • dmchale@gmail.com dave@websolutions.com • @thedavetheory on Twitter • @dmchale on Slack • http://www.binarytemplar.com All images are the property and copyright of their original owners. No ownership is claimed by their use in this presentation