Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Building your translation process

380 views

Published on

How should you do translation in your PHP application? Take leverage of Symfony and PHP-Translation

Published in: Technology
  • Be the first to comment

Building your translation process

  1. 1. Building your translation process Tobias Nyholm @tobiasnyholm @tobiasnyholm
  2. 2. @tobiasnyholm Why?
  3. 3. @tobiasnyholm Language != Country Language != Currency
  4. 4. @tobiasnyholm Why should we listen? • Don’t make my misstakes • Show things I was struggling with • Tell you of different processes
  5. 5. @tobiasnyholm Tobias Nyholm • Full stack unicorn on Happyr.com • Sound of Symfony podcast • Certified Symfony developer • PHP-Stockholm • Open source
  6. 6. @tobiasnyholm Open source PHP-cache HTTPlug Mailgun LinkedIn API clientSwap Stampie BazingaGeocoderBundle PHP-Geocoder FriendsOfApi/boilerplate Guzzle Buzz CacheBundlePSR7 SymfonyBundleTest NSA SimpleBus integrations PSR HTTP clients Neo4j KNP Github API
  7. 7. @tobiasnyholm Open source Happyr/TranslationBundle Happyr/AutoFallbackTranslation JMSTranslationBundle Contributed to LexikTranslationBundle
  8. 8. @tobiasnyholm What is the translation component?
  9. 9. @tobiasnyholm Translator <?php class Translator { public function addResource($format, $resource); public function addLoader(LoaderInterface $loader); public function trans($key, $params, $domain); }
  10. 10. @tobiasnyholm Loaders / Dumpers
  11. 11. @tobiasnyholm So how?
  12. 12. @tobiasnyholm MVP My Very first Page
  13. 13. @tobiasnyholm MVP {% extends "::base.html.twig" %} {% block body %} <h1>Welcome</h1> <p>My first paragraph.</p> {% endblock %}
  14. 14. @tobiasnyholm MVP {% extends "::base.html.twig" %} {% block body %} <h1>{{ 'startpage.headig'|trans }}</h1> <p>{{ 'startpage.paragraph0'|trans }}</p> {% endblock %} startpage: heading: 'Welcome' paragraph0: 'My first paragraph.'
  15. 15. @tobiasnyholm Thank you. Questions?
  16. 16. @tobiasnyholm MVP Add more users to your project?
  17. 17. @tobiasnyholm Your translation source
  18. 18. @tobiasnyholm Your translation source GIT
  19. 19. @tobiasnyholm Your translation source Loco (localise.biz) Transifex Crowdin OpenLocalization POEditor PhraseApp OneSky GetLocalization WebTranslateIt Locale Weblate
  20. 20. @tobiasnyholm Download at each deployment
  21. 21. @tobiasnyholm Uploads?
  22. 22. @tobiasnyholm Translation file format I don’t care (and neither should you)
  23. 23. @tobiasnyholm Use a converter
  24. 24. @tobiasnyholm <?php class Converter { public function __construct(LoaderInterface $loader, $format) { $this->reader = new TranslationReader($loader, $format); $this->writer = new TranslationWriter(); $this->writer->addDumper('xlf', new XliffDumper()); } // ...
  25. 25. @tobiasnyholm public function convert($inputDir, $outputDir, array $locales) { $inputDir = realpath($inputDir); $inStorage = new FileStorage($this->writer, $this->reader, [$inputDir]); $outputDir = realpath($outputDir); $outStorage = new FileStorage($this->writer, $this->reader, [$outputDir]); foreach ($locales as $locale) { $inputCatalogue = new MessageCatalogue($locale); $outputCatalogue = new MessageCatalogue($locale); $inStorage->export($inputCatalogue); foreach ($inputCatalogue->all() as $domain => $messages) { $outputCatalogue->add($messages, $domain); } $outStorage->import($outputCatalogue); } } }
  26. 26. @tobiasnyholm URLs
  27. 27. @tobiasnyholm What about URLs? https://example.com/ https://example.com/sv https://example.com/fr https://example.com/en/price https://example.com/sv/price https://example.com/fr/price https://example.com/my-account
  28. 28. @tobiasnyholm What about URLs? https://example.com/ https://sv.example.com/ https://fr.example.com/ https://example.com/price https://sv.example.com/price https://fr.example.com/price https://example.com/my-account https://sv.example.com/my-account https://fr.example.com/my-account
  29. 29. @tobiasnyholm Show other languages <link rel="alternate" hreflang="sv" href="https://example.com/sv/"> <link rel="alternate" hreflang="en" href="https://example.com/en/"> <link rel="alternate" hreflang="fr" href="https://example.com/fr/">
  30. 30. @tobiasnyholmclass LocaleResolver implements LocaleResolverInterface { public function resolveLocale(Request $request, array $availableLocales) { $locale = $this->getFromQueryParam($request); if (in_array($locale, $availableLocales)) { return $locale; } $locale = $this->getFromSession($request); if (in_array($locale, $availableLocales)) { return $locale; } $locale = $this->getFromCookie($request); if (in_array($locale, $availableLocales)) { return $locale; } $locale = $this->getFromUser(); if (in_array($locale, $availableLocales)) { return $locale; } $locale = $this->getFromIp($request); if (in_array($locale, $availableLocales)) { return $locale; } $locale = $this->getFromAcceptHeader($request, $availableLocales); if (in_array($locale, $availableLocales)) { return $locale; } return; } }
  31. 31. @tobiasnyholm Design
  32. 32. @tobiasnyholm Length of words EN: Save user FI: tallenna käyttäjä
  33. 33. @tobiasnyholm Language switcher
  34. 34. @tobiasnyholm Arabic ‫الترجمة‬ ‫من‬ ‫يكفي‬ ‫ما‬ ‫أدفع‬ ‫لم‬
  35. 35. @tobiasnyholm class WhenRtlLanguageInjectStyle { public function onKernelResponse(FilterResponseEvent $event) { if (!$event->isMasterRequest()) { return; } $locale = $event->getRequest()->getLocale(); if ($this->isRtlLanguage($locale)) { $this->injectToolbar($event->getResponse()); } } private function injectToolbar(Response $response) { $content = $response->getContent(); if (false === $pos = stripos($content, '</HEAD>')) { return; } $toolbar = '<style>html {direction: rtl; unicode-bidi: bidi-override;}</style>'; $content = substr($content, 0, $pos).$toolbar.substr($content, $pos); $response->setContent($content); } private function isRtlLanguage($locale) { return $locale === 'ar'; } }
  36. 36. @tobiasnyholm Translations in JavaScript
  37. 37. @tobiasnyholm {% block toggle_button %} <a data-show-label="{{ 'show'|trans }}" data-hide-label="{{ 'hide'|trans }}” > {{ 'show'|trans }} </a> {% endblock %}
  38. 38. @tobiasnyholm
  39. 39. @tobiasnyholm // translation.js.twig var Trans = { show: "{{ 'show'|trans }}", hide: "{{ 'hide'|trans }}" }; // Use it like: console.log(Trans.show);
  40. 40. @tobiasnyholm The process
  41. 41. @tobiasnyholm Adding new translation
  42. 42. @tobiasnyholm Extract from source
  43. 43. @tobiasnyholm
  44. 44. @tobiasnyholm Feature branches Never change translations
  45. 45. @tobiasnyholm Feature branches
  46. 46. @tobiasnyholm Change translations Key English Swedish Russian user.apply.heading Foo Bar Baz user.apply.get_started.button Start Börja начало
  47. 47. @tobiasnyholm Change translations Key English Swedish Russian user.apply.heading Foo Bar Baz user.apply.get_started.button Read more Börja начало
  48. 48. @tobiasnyholm Deploy new translations What to do when new translation is added? A - Wait for all translators to finish before you 
 deploy your changes C - Use Google translate B - Use your fallback locale
  49. 49. @tobiasnyholm Prioritize translation keys
  50. 50. class TranslatorLogger { public function onTerminate(PostResponseEvent $event) { $messages = $this->translator->getCollectedMessages(); $missing = []; $fallback = []; $valid = []; //Sort the messages foreach ($messages as $message) { if ($message['state'] === DataCollectorTranslator::MESSAGE_MISSING) { $missing[] = $message; } elseif ($message['state'] === DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK) { $fallback[] = $message; } else { $valid[] = $message; } } $request = $event->getRequest(); $data = [ 'locale' => $request->getLocale(), 'host' => $request->getHost(), 'url' => $request->getUri(), 'messages' => $messages, ]; // Store in cache or send somewhere } }
  51. 51. @tobiasnyholm Context
  52. 52. http://php-translation.readthedocs.io/en/latest/best-practice/index.html
  53. 53. @tobiasnyholm English is easy - Languages are hard difficult
  54. 54. @tobiasnyholm Do not reuse keys
  55. 55. @tobiasnyholm “Users” - heading “Users” - link
  56. 56. @tobiasnyholm Clusivity We’ve just won the lottery
  57. 57. @tobiasnyholm Direction
  58. 58. @tobiasnyholm Eskimos 50 words for snow
  59. 59. @tobiasnyholm Swedish Swedish English Val Whale Val Election Val Choice
  60. 60. @tobiasnyholm Do not reuse keys
  61. 61. @tobiasnyholm Do not reuse keys
  62. 62. @tobiasnyholm DO NOT REUSE KEYS
  63. 63. @tobiasnyholm DO NOT REUSE KEYS (unless when you do)
  64. 64. @tobiasnyholm Work now Work later
  65. 65. @tobiasnyholm Tools
  66. 66. @tobiasnyholm Tools PHP-Translation
 GUI, Extractor, Saas integration, AutoFallback JMSTranslatorBundle
 GUI, Extractor LexikTranslationBundle
 GUI, DB-access
  67. 67. @tobiasnyholm CLI
  68. 68. @tobiasnyholm CLI
  69. 69. @tobiasnyholm
  70. 70. @tobiasnyholm Questions? https://joind.in/talk/92db0
  71. 71. {% extends "::base.html.twig" %} {% block body %} <h1>{{ 'startpage.headig'|trans }}</h1> <p>{{ 'startpage.paragraph0'|trans }}</p> <p> {{ 'startpage.paragraph1'|trans }} <a href="http://tnyholm.se" class="foo"> {{ 'startpage.clicking_here'|trans }} </a> </p> {% endblock %}
  72. 72. {% extends "::base.html.twig" %} {% block body %} <h1>{{ 'startpage.headig'|trans }}</h1> <p>{{ 'startpage.paragraph0'|trans }}</p> <p>{% trans with { '%url_start%':'<a href="http://tnyholm.se" class="foo">', '%url_end%':'</a>' } %}startpage.paragraph1{% endtrans %}</p> {% endblock %}
  73. 73. startpage: heading: 'Welcome' paragraph0: 'My first paragraph.' paragraph1: 'Visit my website by %url_start %clicking here%url_end%.'
  74. 74. @tobiasnyholm Questions? https://joind.in/talk/92db0
  75. 75. @tobiasnyholm Questions? https://joind.in/talk/92db0
  76. 76. {% extends "::base.html.twig" %} {% block body %} <img src="{{ asset('images/foo'~app.request.locale~'.jpg') }}"> {% endblock %}
  77. 77. @tobiasnyholm Questions? https://joind.in/talk/92db0

×