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 - Verona 2018

128 views

Published on

How to build your translation process in modern web applications

Published in: Technology
  • Be the first to comment

Building your translation process - Verona 2018

  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 • Certified Symfony developer • Symfony core member • 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 Puli Assert
  7. 7. @tobiasnyholm Open source Happyr/TranslationBundle Happyr/AutoFallbackTranslation JMSTranslationBundle Contributed to LexikTranslationBundle
  8. 8. @tobiasnyholm What is the Symfony 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 Lightning round
  27. 27. @tobiasnyholm URLs
  28. 28. @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
  29. 29. @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
  30. 30. @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/">
  31. 31. @tobiasnyholm Show other languages <link rel="alternate" hreflang="sv" href=“https://example.com/sv/mitt-liv“> <link rel="alternate" hreflang="en" href=“https://example.com/en/my-life“> <link rel="alternate" hreflang="fr" href=“https://example.com/fr/ma-vie”>
  32. 32. @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; } }
  33. 33. @tobiasnyholm Design
  34. 34. @tobiasnyholm Length of words EN: Save user FI: tallenna käyttäjä
  35. 35. @tobiasnyholm Language switcher <a href=“?hl=da”>Dansk</a>
 <a href=“?hl=en”>English</a>
 <a href=“?hl=ru”> Русский </a>
 <a href=“?hl=sv”>Svenska</a>
  36. 36. @tobiasnyholm Arabic ‫الترجمة‬ ‫من‬ ‫يكفي‬ ‫ما‬ ‫أدفع‬ ‫لم‬
  37. 37. @tobiasnyholm class WhenRtlLanguageInjectStyle { public function onKernelResponse(FilterResponseEvent $event) { if (!$event->isMasterRequest()) { return; } $locale = $event->getRequest()->getLocale(); if ($this->isRtlLanguage($locale)) { $this->injectStyle($event->getResponse()); } } private function injectStyle(Response $response) { $content = $response->getContent(); if (false === $pos = stripos($content, '</HEAD>')) { return; } $style = '<style>html {direction: rtl; unicode-bidi: bidi-override;}</style>'; $content = substr($content, 0, $pos).$style.substr($content, $pos); $response->setContent($content); } private function isRtlLanguage($locale) { return $locale === 'ar'; } }
  38. 38. @tobiasnyholm Translations in JavaScript
  39. 39. @tobiasnyholm {% block toggle_button %} <a data-show-label="{{ 'show'|trans }}" data-hide-label="{{ 'hide'|trans }}” > {{ 'show'|trans }} </a> {% endblock %}
  40. 40. @tobiasnyholm
  41. 41. @tobiasnyholm // translation.js.twig var Trans = { show: "{{ 'show'|trans }}", hide: "{{ 'hide'|trans }}" }; // Use it like: console.log(Trans.show);
  42. 42. @tobiasnyholm The process
  43. 43. @tobiasnyholm Adding new translation
  44. 44. @tobiasnyholm Extract from source
  45. 45. @tobiasnyholm
  46. 46. @tobiasnyholm Feature branches Never change translations
  47. 47. @tobiasnyholm Feature branches
  48. 48. @tobiasnyholm Change translations Key English Swedish Russian user.apply.heading Foo Bar Baz user.apply.get_started.button Start Börja начало
  49. 49. @tobiasnyholm Change translations Key English Swedish Russian user.apply.heading Foo Bar Baz user.apply.get_started.button Read more Börja начало
  50. 50. @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
  51. 51. @tobiasnyholm Prioritize translation keys
  52. 52. 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 } }
  53. 53. @tobiasnyholm Context
  54. 54. http://php-translation.readthedocs.io/en/latest/best-practice/index.html
  55. 55. @tobiasnyholm English is easy - Languages are hard difficult
  56. 56. @tobiasnyholm Do not reuse keys
  57. 57. @tobiasnyholm “Users” - heading “Users” - link
  58. 58. @tobiasnyholm Clusivity We’ve just won the lottery
  59. 59. @tobiasnyholm Direction
  60. 60. @tobiasnyholm Eskimos 50 words for snow
  61. 61. @tobiasnyholm Swedish Swedish English Val Whale Val Election Val Choice
  62. 62. @tobiasnyholm Do not reuse keys
  63. 63. @tobiasnyholm Do not reuse keys
  64. 64. @tobiasnyholm DO NOT REUSE KEYS
  65. 65. @tobiasnyholm DO NOT REUSE KEYS (unless when you do)
  66. 66. @tobiasnyholm Work now Work later
  67. 67. @tobiasnyholm Tools
  68. 68. @tobiasnyholm Tools PHP-Translation
 GUI, Extractor, Saas integration, AutoFallback JMSTranslatorBundle
 GUI, Extractor LexikTranslationBundle
 GUI, DB-access
  69. 69. @tobiasnyholm CLI
  70. 70. @tobiasnyholm CLI
  71. 71. @tobiasnyholm
  72. 72. @tobiasnyholm Questions? https://joind.in/talk/8c80a
  73. 73. {% 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 %}
  74. 74. {% 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 %}
  75. 75. startpage: heading: 'Welcome' paragraph0: 'My first paragraph.' paragraph1: 'Visit my website by %url_start%clicking here%url_end%.'
  76. 76. @tobiasnyholm Questions? https://joind.in/talk/8c80a
  77. 77. @tobiasnyholm Questions? https://joind.in/talk/8c80a
  78. 78. {% extends "::base.html.twig" %} {% block body %} <img src="{{ asset('images/foo'~app.request.locale~'.jpg') }}"> {% endblock %}
  79. 79. @tobiasnyholm Questions? https://joind.in/talk/8c80a

×