{"id":16169,"date":"2022-03-25T08:00:06","date_gmt":"2022-03-25T07:00:06","guid":{"rendered":"https:\/\/phrase.com\/blog\/?p=16169"},"modified":"2024-02-19T16:22:01","modified_gmt":"2024-02-19T15:22:01","slug":"localize-software","status":"publish","type":"post","link":"https:\/\/phrase.com\/blog\/posts\/localize-software\/","title":{"rendered":"How to Localize Software at Scale: A Step-by-Step Guide"},"content":{"rendered":"\n<div id=\"acf\/text-block_0bb8548f43fb2464c8408cf90be6296a\" class=\"pxblock pxblock--text alignfull spacing--default bg--white\">\n\n\t\n\t<div class=\"container\">\n\t\t<div class=\"wysiwyg animate-in\">\n\t\t\t<p>If our business is lucky enough to make it past its first few years, we often need to address the next hurdle: scaling. In this guide, we take a look at scaling from the perspective of internationalization (i18n) and localization (l10n). We address workflow efficiency and how <a href=\"https:\/\/phrase.com\/blog\/posts\/localization-technology\/\">localization technology<\/a> can save us time and money as we scale, keeping us competitive through a laser focus on our core offering.<\/p>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_69_1 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Overview<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#our-minimum-viable-product-mvp\" title=\"Our minimum viable product (MVP)\">Our minimum viable product (MVP)<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#localizing-software\" title=\"Localizing software\">Localizing software<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#localizing-the-mobile-app\" title=\"Localizing the mobile app\">Localizing the mobile app<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#localizing-the-webdesktop-app\" title=\"Localizing the web\/desktop app\">Localizing the web\/desktop app<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#scaling-issues-analyzing-our-localization-solution\" title=\"Scaling issues: analyzing our localization solution\">Scaling issues: analyzing our localization solution<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#taking-the-plunge-a-software-localization-platform-to-the-rescue\" title=\"Taking the plunge: a software localization platform to the rescue\">Taking the plunge: a software localization platform to the rescue<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#project-setup\" title=\"Project setup\">Project setup<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#connecting-the-phrase-cli\" title=\"Connecting the Phrase CLI\">Connecting the Phrase CLI<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#the-translator-experience\" title=\"The translator experience\">The translator experience<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#connecting-developers-and-translators\" title=\"Connecting developers and translators\">Connecting developers and translators<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#github-sync-and-continuous-localization\" title=\"GitHub sync and continuous localization\">GitHub sync and continuous localization<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#generating-a-github-access-token\" title=\"Generating a GitHub access token\">Generating a GitHub access token<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#auto-importing-using-a-webhook\" title=\"Auto-importing using a webhook\">Auto-importing using a webhook<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#continuous-integration-continuous-localization\" title=\"Continuous integration, continuous localization\">Continuous integration, continuous localization<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#saving-time-for-translators-translation-memory\" title=\"Saving time for translators: translation memory\">Saving time for translators: translation memory<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/phrase.com\/blog\/posts\/localize-software\/#wrapping-up-our-tutorial-on-how-to-localize-software-at-scale\" title=\"Wrapping up our tutorial on how to localize software at scale\">Wrapping up our tutorial on how to localize software at scale<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"our-minimum-viable-product-mvp\"><\/span>Our minimum viable product (MVP)<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Let\u2019s say we\u2019re a fantastic new e-commerce startup called <em>HandiRaft,<\/em> with a goal of connecting artisans with customers who want to buy bespoke crafts. We\u2019ve decided that our MPV will include iOS and Android apps, and a web app for desktop users. To reduce risk and validate our core offering as quickly as possible, we\u2019ve built our mobile apps with Flutter and our web app with React.<\/p>\n<div><strong style=\"color: #ff6600;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16171\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-before-l10n-542x1024.png\" alt=\"Single codebase for both Android and iOS saves us time and money | Phrase\" width=\"320\" height=\"605\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-before-l10n-542x1024.png 542w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-before-l10n-159x300.png 159w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-before-l10n-768x1452.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-before-l10n-813x1536.png 813w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-before-l10n-1083x2048.png 1083w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-before-l10n.png 1530w\" sizes=\"(max-width: 320px) 100vw, 320px\" \/><\/strong><\/div>\n<p class=\"utility\" style=\"text-align: center;\">Our Flutter mobile MVP: one codebase for both Android and iOS saves us time and money<\/p>\n<div><strong style=\"color: #ff6600;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16172 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/desktop-before-l10n.png\" alt=\"Desktop MVP with React | Phrase\" width=\"2408\" height=\"1747\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/desktop-before-l10n.png 2408w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/desktop-before-l10n-300x218.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/desktop-before-l10n-1024x743.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/desktop-before-l10n-768x557.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/desktop-before-l10n-1536x1114.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/desktop-before-l10n-2048x1486.png 2048w\" sizes=\"(max-width: 2408px) 100vw, 2408px\" \/><\/strong><\/div>\n<p class=\"utility\" style=\"text-align: center;\">Our desktop MVP with React<\/p>\n<p>An offering like ours has some technical challenges right out of the gate:<\/p>\n<ul>\n<li>Handling payments, including security, which we can outsource to a service like Stripe.<\/li>\n<li>Creating an admin panel for creators, which our MVP would include in the web\/desktop app, with some roles and permission management for buyers and creators.<\/li>\n<li>Handling the ecommerce experience, including buyer accounts, the shopping cart, orders, and returns\/refunds.<\/li>\n<li>Ensuring we have excellent customer support, which we can outsource the tech solution for while keeping our support staff in-house for the best customer experience.<\/li>\n<\/ul>\n<div><strong style=\"color: #ff6600;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16625\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/app-architecture-before-l10n.jpg\" alt=\"App architecture before localization | Phrase\" width=\"500\" height=\"500\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/app-architecture-before-l10n.jpg 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/app-architecture-before-l10n-300x300.jpg 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/app-architecture-before-l10n-150x150.jpg 150w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/app-architecture-before-l10n-768x768.jpg 768w\" sizes=\"(max-width: 500px) 100vw, 500px\" \/><\/strong><\/div>\n<p class=\"utility\" style=\"text-align: center;\">A simplified view of our app architecture<\/p>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> You can get the source code for our mocked-up apps from the companion GitHub repos:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/PhraseApp-Blog\/flutter-i18n-at-scale\">Flutter app repo<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/PhraseApp-Blog\/react-i18n-at-scale-2022\">React app repo<\/a><\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"localizing-software\"><\/span>Localizing software<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Connecting buyers and artisans all over the world means taking a global approach to our offering. At the very least, we need to internationalize our public-facing apps and localize them for our most prominent target markets. This isn\u2019t too difficult with Flutter and React.<\/p>\n<p>\ud83e\udd3f <em>Go deeper<\/em> \u00bb Our\u00a0<a href=\"https:\/\/phrase.com\/blog\/posts\/software-localization\/\">Complete Guide to Software Localization<\/a> goes into much more detail regarding what software localization is, its strategic importance for your business, and best practices for optimizing your localization workflows.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"localizing-the-mobile-app\"><\/span>Localizing the mobile app<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Flutter includes a robust first-party i18n library, and it can kickstart our mobile app i18n in a hurry. Here\u2019s the skinny:<\/p>\n<ul>\n<li>We have localization ARB files, one per supported locale.<\/li>\n<li>We wire up the i18n library to our app and use its built-in code generation to load translation strings from these ARB files, instead of hard-coding them in our UI.<\/li>\n<\/ul>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\" data-enlighter-linenumbers=\"false\">.\r\n\u2514\u2500\u2500 lib\/\r\n    \u251c\u2500\u2500 l10n\/\r\n    \u2502   \u251c\u2500\u2500 app_en.arb # English translations\r\n    \u2502   \u251c\u2500\u2500 app_ar.arb # Arabic translations\r\n    \u2502   \u2514\u2500\u2500 ...        # etc.\r\n    \u2502\r\n    \u251c\u2500\u2500 main.dart      # Connects the Flutter localizations package\r\n    \u2502                  # to our app\r\n    \u2502\r\n    \u2514\u2500\u2500 widgets\/\r\n        \u251c\u2500\u2500 creator_card.dart # Widgets import localization packages;\r\n        \u2502                     # resolve and use current locale\r\n        \u2502                     # translation strings\r\n        \u2502\r\n        \u2514\u2500\u2500 ...               # etc.\r\n<\/pre>\n<p>Here\u2019s what our <code>main.dart<\/code> file looks like:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-highlight=\"2,3,17,19,20,21,22,23,24,25,26,27,28\">import 'package:flutter\/material.dart';\r\nimport 'package:flutter_localizations\/flutter_localizations.dart';\r\nimport 'package:flutter_gen\/gen_l10n\/app_localizations.dart';\r\nimport 'pages\/home_page.dart';\r\nvoid main() {\r\n  runApp(const MyApp());\r\n}\r\nclass MyApp extends StatelessWidget {\r\n  const MyApp({Key? key}) : super(key: key);\r\n  @override\r\n  Widget build(BuildContext context) {\r\n    return MaterialApp(\r\n      onGenerateTitle: (context) {\r\n        return AppLocalizations.of(context)!.appTitle;\r\n      },\r\n      localizationsDelegates: const [\r\n        AppLocalizations.delegate,\r\n        GlobalMaterialLocalizations.delegate,\r\n        GlobalWidgetsLocalizations.delegate,\r\n        GlobalCupertinoLocalizations.delegate,\r\n      ],\r\n      supportedLocales: const [\r\n        Locale('en', ''),\r\n        Locale('ar', ''),\r\n      ],\r\n      theme: ThemeData(\r\n        primarySwatch: Colors.deepOrange,\r\n      ),\r\n      home: const HomePage(),\r\n    );\r\n  }\r\n}\r\n<\/pre>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The full code of this app is under the <code>flutter_app<\/code> directory of our <a href=\"https:\/\/github.com\/PhraseApp-Blog\/i18n-at-scale-2022\/tree\/main\/flutter_app\">companion GitHub repo<\/a>.<\/p>\n<p>Our translation <code>.arb<\/code> files are basically JSON.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"dbc19ed7-3c83-4507-a34f-1921cfdcc1c4\" data-enlighter-title=\"lib\/l10n\/app_en.arb\">{\r\n  \"appTitle\": \"HandiCraft\",\r\n  \"featuredCreators\": \"Featured Creators\",\r\n  \"searchPlaceholder\": \"Find creator or product\",\r\n  \"topRated\": \"Top Rated\",\r\n  \"featured\": \"Featured\",\r\n  \"search\": \"Search\",\r\n  \"cart\": \"Cart\",\r\n  \"account\": \"Account\",\r\n  \/\/ ...\r\n}\r\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"b2069a2c-f14a-4d7c-9bae-4efce62bde0a\" data-enlighter-title=\"lib\/l10n\/app_ar.arb\">{\r\n  \"appTitle\": \"\u0647\u0627\u0646\u062f\u064a\u0643\u0631\u0627\u0641\u062a\",\r\n  \"featuredCreators\": \"\u0627\u0644\u062d\u0631\u0641\u064a\u064a\u0646 \u0627\u0644\u0645\u0645\u064a\u0632\u064a\u0646\",\r\n  \"searchPlaceholder\": \"\u0627\u0628\u062d\u062b \u0639\u0646 \u062d\u0631\u0641\u064a \u0623\u0648 \u0645\u0646\u062a\u062c\",\r\n  \"topRated\": \"\u0627\u0644\u0623\u0639\u0644\u0649 \u062a\u0642\u064a\u064a\u0645\u064b\u0627\",\r\n  \"featured\": \"\u0645\u062a\u0645\u064a\u0632\",\r\n  \"search\": \"\u0628\u062d\u062b\",\r\n  \"cart\": \"\u0639\u0631\u0628\u0629 \u0627\u0644\u062a\u0633\u0648\u0642\",\r\n  \"account\": \"\u0627\u0644\u062d\u0633\u0627\u0628\",\r\n  \/\/ ...\r\n}\r\n<\/pre>\n<p>Our widgets use prebuilt localization libraries to translate their UI:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"2ffe3aac-66f0-4367-ac17-f64ed535cde0\" data-enlighter-title=\"lib\/widgets\/make_bottom_nav_bar.dart\" data-enlighter-highlight=\"2,7,8,9,18,22\">import 'package:flutter\/material.dart';\r\nimport 'package:flutter_gen\/gen_l10n\/app_localizations.dart';\r\nBottomNavigationBar makeBottomNavBar(BuildContext context) {\r\n  var theme = Theme.of(context);\r\n  \/\/ Ensures that translations matching the currently active\r\n  \/\/ locale are loaded\r\n  var t = AppLocalizations.of(context)!;\r\n  return BottomNavigationBar(\r\n    type: BottomNavigationBarType.fixed,\r\n    selectedItemColor: theme.primaryColorDark,\r\n    selectedFontSize: 13,\r\n    unselectedFontSize: 13,\r\n    items: [\r\n      BottomNavigationBarItem(\r\n        label: t.featured,          \/\/ Pulls translation\r\n        icon: const Icon(Icons.star),\r\n      ),\r\n      BottomNavigationBarItem(\r\n        label: t.search,            \/\/ Pulls translation\r\n        icon: const Icon(Icons.search),\r\n      ),\r\n      \/\/ ...\r\n    ],\r\n  );\r\n}\r\n<\/pre>\n<div><strong style=\"color: #ff6600;\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-16174 aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-after-l10n-548x1024.png\" alt=\"An app localized in Arabic | Phrase\" width=\"320\" height=\"598\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-after-l10n-548x1024.png 548w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-after-l10n-161x300.png 161w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-after-l10n-768x1434.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-after-l10n-822x1536.png 822w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-after-l10n-1097x2048.png 1097w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/mobile-app-after-l10n.png 1570w\" sizes=\"(max-width: 320px) 100vw, 320px\" \/><\/strong><\/div>\n<p class=\"utility\" style=\"text-align: center;\">Our mobile app localized in Arabic<\/p>\n<p>\ud83e\udd3f <em>Go deeper \u00bb\u00a0<\/em> <a href=\"https:\/\/phrase.com\/blog\/posts\/flutter-localization\/\">A Guide to Flutter Localization<\/a> goes into localizing an app with the Flutter localization package in much more detail.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"localizing-the-webdesktop-app\"><\/span>Localizing the web\/desktop app<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>React doesn\u2019t offer i18n out of the box, but the very popular <a href=\"https:\/\/www.i18next.com\/\">i18next<\/a> library has an excellent <a href=\"https:\/\/react.i18next.com\/\">React integration<\/a> we can use. The localization process is similar to our mobile app: We move our hard-coded UI strings to per-locale translation files and load the translation file corresponding to the active locale.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\" data-enlighter-linenumbers=\"false\">.\r\n\u251c\u2500\u2500 public\/\r\n\u2502   \u251c\u2500\u2500 index.html\r\n\u2502   \u2514\u2500\u2500 locales\/\r\n\u2502       \u251c\u2500\u2500 en\/\r\n\u2502       \u2502   \u2514\u2500\u2500 translation.json # English translations\r\n\u2502       \u251c\u2500\u2500 ar\/\r\n\u2502       \u2502   \u2514\u2500\u2500 translaton.json  # Arabic translations\r\n\u2502       \u2502\r\n\u2502       \u2514\u2500\u2500 ...                  # etc.\r\n\u2514\u2500\u2500 src\/\r\n    \u251c\u2500\u2500 index.js                 # Loads i18n.js to initialize it\r\n    \u251c\u2500\u2500 services\/\r\n    \u2502   \u2514\u2500\u2500 i18n.js              # Bootstraps our i18n library\r\n    \u2514\u2500\u2500 features\/\r\n        \u251c\u2500\u2500 Creators\/\r\n        \u2502   \u2514\u2500\u2500 CreatorCard.js   # Imports i18n library and uses\/\r\n        \u2502                        # current locale translation\r\n        \u2502                        # strings\r\n        \u2502\r\n        \u2514\u2500\u2500 ...                  # etc.\r\n<\/pre>\n<p>Our i18n bootstrap file basically initializes the i18next library.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"f0927cd4-28c9-4baf-ae0c-7af279132184\" data-enlighter-title=\"src\/services\/i18n.js\">import i18next from \"i18next\";\r\nimport { initReactI18next } from \"react-i18next\";\r\nimport HttpApi from \"i18next-http-backend\";\r\ni18next\r\n  .use(initReactI18next)\r\n  .use(HttpApi)\r\n  .init({\r\n    debug: true,\r\n    lng: \"en\",\r\n    interpolation: {\r\n      escapeValue: false,\r\n    },\r\n  });\r\nexport default i18next;\r\n<\/pre>\n<p>\ud83d\udd17 <em>Resource \u00bb<\/em> The full code of this app is under the <code>react_app<\/code> directory of our <a href=\"https:\/\/github.com\/PhraseApp-Blog\/i18n-at-scale-2022\/tree\/main\/react_app\">companion GitHub repo<\/a>.<br \/>\nThe <code>index.js<\/code> entry point simply imports the file to init the library.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-highlight=\"3,8,9,10,12\">import React from \"react\";\r\nimport ReactDOM from \"react-dom\";\r\nimport \".\/services\/i18n\";\r\nimport App from \".\/App\";\r\nReactDOM.render(\r\n  &lt;React.StrictMode&gt;\r\n    {\/* We use React Suspense to show a loading message\r\n        while our translation file downloads *\/}\r\n    &lt;React.Suspense fallback=\"Loading...\"&gt;\r\n      &lt;App \/&gt;\r\n    &lt;\/React.Suspense&gt;\r\n  &lt;\/React.StrictMode&gt;,\r\n  document.getElementById(\"root\")\r\n);\r\n<\/pre>\n<p>i18next will automatically load our translation files from the URI <code>\/locales\/{locale}\/translation.json<\/code>. These translation files look a lot like our Flutter ARB files.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"eb3871a9-c5a7-4a21-b509-2c0155491c76\" data-enlighter-title=\"public\/locales\/en\/translation.json\">{\r\n  \"appTitle\": \"HandiCraft\",\r\n  \"featuredCreators\": \"Featured Creators\",\r\n  \"searchPlaceholder\": \"Find creator or product\",\r\n  \"topRated\": \"Top Rated\",\r\n  \"featured\": \"Featured\",\r\n  \"search\": \"Search\",\r\n  \"cart\": \"Cart\",\r\n  \"account\": \"Account\",\r\n  \"copyright\": \"Copyright\",\r\n  \/\/ ...\r\n}\r\n<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"6480b755-3090-4b4a-9b4f-ee1106879ace\" data-enlighter-title=\"public\/locales\/ar\/translation.json\">{\r\n  \"appTitle\": \"\u0647\u0627\u0646\u062f\u064a\u0643\u0631\u0627\u0641\u062a\",\r\n  \"featuredCreators\": \"\u0627\u0644\u062d\u0631\u0641\u064a\u064a\u0646 \u0627\u0644\u0645\u0645\u064a\u0632\u064a\u0646\",\r\n  \"searchPlaceholder\": \"\u0627\u0628\u062d\u062b \u0639\u0646 \u062d\u0631\u0641\u064a \u0623\u0648 \u0645\u0646\u062a\u062c\",\r\n  \"topRated\": \"\u0627\u0644\u0623\u0639\u0644\u0649 \u062a\u0642\u064a\u064a\u0645\u064b\u0627\",\r\n  \"featured\": \"\u0645\u062a\u0645\u064a\u0632\",\r\n  \"search\": \"\u0628\u062d\u062b\",\r\n  \"cart\": \"\u0639\u0631\u0628\u0629 \u0627\u0644\u062a\u0633\u0648\u0642\",\r\n  \"account\": \"\u0627\u0644\u062d\u0633\u0627\u0628\",\r\n  \"copyright\": \"\u062d\u0642\u0648\u0642 \u0627\u0644\u0646\u0634\u0631\",\r\n  \/\/ ...\r\n}\r\n<\/pre>\n<p>Our React components then just import the i18next instance and use it to display UI strings translated in the active locale.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"04dd1b6a-9466-4136-8f82-f9470f086cc3\" data-enlighter-title=\"src\/features\/Creators\/CreatorCard.js\" data-enlighter-highlight=\"10,13,21,22\">\/\/ MUI framework: Material components for React\r\nimport {\r\n  Card,\r\n  CardContent,\r\n  Typography,\r\n  \/\/ ...\r\n} from \"@mui\/material\";\r\n\/\/ Imports the initialized i18next instance\r\nimport { useTranslation } from \"react-i18next\";\r\nexport default function CreatorCard(props) {\r\n  const { t } = useTranslation();\r\n  return (\r\n    &lt;Card&gt;\r\n      {\/* ... *\/}\r\n      &lt;CardContent&gt;\r\n        &lt;Typography&gt;\r\n          \/\/ Pulls translation for active locale\r\n          {t(\"specialties\")}\r\n        &lt;\/Typography&gt;\r\n        {\/* ... *\/}\r\n      &lt;\/CardContent&gt;\r\n    &lt;\/Card&gt;\r\n  );\r\n}\r\n<\/pre>\n<p>\ud83e\udd3f <em>Go deeper \u00bb<\/em> We cover localizing React apps with i18next in a lot of detail in our <a href=\"https:\/\/phrase.com\/blog\/posts\/localizing-react-apps-with-i18next\/\">Guide to React Localization with i18next<\/a>.<\/p>\n<p>Et voil\u00e0. It takes a little bit of work, but the payoff is reaching a wider global audience.<\/p>\n<div><strong style=\"color: #ff6600;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16175 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/02\/locale-switcher.gif\" alt=\"Our localized desktop app | Phrase\" width=\"600\" height=\"478\" \/><\/strong><\/div>\n<p class=\"utility\" style=\"text-align: center;\">Our localized desktop app<\/p>\n<h2><span class=\"ez-toc-section\" id=\"scaling-issues-analyzing-our-localization-solution\"><\/span>Scaling issues: analyzing our localization solution<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>We\u2019ve been able to localize our app into multiple languages, and our buyer and creator base has grown dramatically. In fact, we\u2019ve secured some good funding and we\u2019re ready to scale up our offering and go deeper into the UX and the value we can provide to our community. There\u2019s a lot to tackle, including localization.<\/p>\n<div><strong style=\"color: #ff6600;\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16626\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/app-architecture-after-l10n.jpg\" alt=\"App architecture including localization | Phrase\" width=\"500\" height=\"500\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/app-architecture-after-l10n.jpg 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/app-architecture-after-l10n-300x300.jpg 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/app-architecture-after-l10n-150x150.jpg 150w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/app-architecture-after-l10n-768x768.jpg 768w\" sizes=\"(max-width: 500px) 100vw, 500px\" \/><\/strong><\/div>\n<p class=\"utility\" style=\"text-align: center;\">Our current architecture, including localization<\/p>\n<p>While our current i18n\/l10n solution has kept us light on our feet, our product teams may start to complain about annoyingly inefficient workflows with the ever-increasing volume of content and languages being added:<\/p>\n<ul>\n<li>Translators have to manage text files that travel back and forth to developers to integrate into app codebases.<\/li>\n<li>Designers have to communicate screen updates to both developers and translators.<\/li>\n<li>Developers find they often need to provide screenshots to translators to give them context around new translation strings.<\/li>\n<li>Translators have to manage duplicate strings in the same app, and duplication across apps.<\/li>\n<li>Translations add complexity to managing feature flags, branches, and versions for our apps.<\/li>\n<\/ul>\n<p>To solve these issues, we consider building our own localization admin backend. However, our very expensive engineering time would then be spread between this new backend and our much-needed updates across our public-facing apps. We would also have to maintain this backend in the future, which is time, effort, and attention that could be better spent on broadening and refining our core offering.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"taking-the-plunge-a-software-localization-platform-to-the-rescue\"><\/span>Taking the plunge: a software localization platform to the rescue<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Lucky for us, others have done the hard work of clearing the workflow bottlenecks we face when we scale our localization. These software localization platforms provide a slew of localization tech services: from syncing translations and web consoles for translators to all kinds of automation and integration.<\/p>\n<p>By outsourcing our localization tech to a localization platform, we can focus on our core offering, ensuring that our time and effort are directed to providing the best product for our customers. We\u2019re a bit biased, but we think <a href=\"https:\/\/phrase.com\/platform\/strings\/\">Phrase Strings<\/a> is one of the best software localization platforms on the market, so we\u2019ll go ahead and use it in this tutorial.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> For brevity, we will largely cover connecting our Flutter app to Phrase Strings. The steps for connecting our React app are almost identical. We will also focus on setting up Phrase Strings, GitHub syncing, and managing translation duplication. There are <a href=\"https:\/\/phrase.com\/roles\/\">Phrase solutions<\/a> for\u00a0all the problems we listed above.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"project-setup\"><\/span>Project setup<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>If you don\u2019t have an organization in Phrase Strings set up yet, you can get a free trial so you can jump in and play around with it yourself. Once we have access, we can create two projects: one for our Flutter app and one for our React app.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16464 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-create-project-1024x1024.png\" alt=\"Adding a project to Phrase | Phrase\" width=\"1024\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-create-project-1024x1024.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-create-project-300x300.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-create-project-150x150.png 150w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-create-project-768x768.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-create-project.png 1264w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/>After saving, we can add English and Arabic as languages to our project, and skip the rest of the setup.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16466 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-skip-setup-1024x558.png\" alt=\"Phrase project setup | Phrase\" width=\"1024\" height=\"558\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-skip-setup-1024x558.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-skip-setup-300x164.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-skip-setup-768x419.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-skip-setup-1536x837.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-skip-setup-2048x1117.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"connecting-the-phrase-cli\"><\/span>Connecting the Phrase CLI<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>To begin syncing our translations between our apps and our Phrase projects, we need to initialize the projects using the Phrase command line interface (CLI). Installing the CLI is straightforward, and you can easily find <a href=\"https:\/\/support.phrase.com\/hc\/en-us\/articles\/5784093863964-CLI-Installation-Strings\">instructions<\/a> to do so for your operating system of choice. I\u2019m on macOS, and I\u2019ll use the <a href=\"https:\/\/brew.sh\/\">Homebrew<\/a> package manager to install the Phrase CLI from my command line:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\"># Add Phrase Homebrew repository\r\n$ brew tap phrase\/brewed\r\n# Install Phrase CLI\r\n$ brew install phrase\r\n<\/pre>\n<p>Once the CLI is installed, we can use it to connect each of our projects. For example, we can connect our Flutter project by navigating to its root directory in the command line and running the following command.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\">$ phrase init\r\n<\/pre>\n<p>At this point, we\u2019re asked for an access token.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16467 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-ask-for-token-1024x669.png\" alt=\"The CLI parrot is asking us for an access token | Phrase\" width=\"1024\" height=\"669\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-ask-for-token-1024x669.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-ask-for-token-300x196.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-ask-for-token-768x502.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-ask-for-token-1536x1004.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-ask-for-token-2048x1339.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><span style=\"color: #505050; font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif; font-weight: 400;\">Access tokens are generated from our Phrase organization page. Once logged in to Phrase, we can click our name near the top-right of the screen and select <\/span><em style=\"color: #505050; font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif; font-weight: 400;\">Profile Settings<\/em><span style=\"color: #505050; font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif; font-weight: 400;\"> from the dropdown. We can then click the <\/span><em style=\"color: #505050; font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif; font-weight: 400;\">Access Tokens<\/em><span style=\"color: #505050; font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif; font-weight: 400;\"> tab near the top of the Profile Settings page, and click the <\/span><em style=\"color: #505050; font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif; font-weight: 400;\">Generate<\/em><span style=\"color: #505050; font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', 'Liberation Sans', Arial, sans-serif; font-weight: 400;\"> button to get a new token.<\/span><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16468 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-tokens-1024x558.png\" alt=\"Generating a new access token from the Phrase console | Phrase\" width=\"1024\" height=\"558\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-tokens-1024x558.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-tokens-300x164.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-tokens-768x419.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-tokens-1536x837.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-tokens-2048x1117.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/>Token generated, we can copy and paste it into the CLI prompt to continue project initialization. The next step is choosing the Phrase project to link to our app. We\u2019ll pick our Flutter Handiraft project, of course.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16469 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-select-project-1024x669.png\" alt=\"Entering project number to select it from the Phrase CLI | Phrase\" width=\"1024\" height=\"669\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-select-project-1024x669.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-select-project-300x196.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-select-project-768x502.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-select-project-1536x1004.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-select-project-2048x1339.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/>After selecting the translation file format, ARB for our Flutter project, we can provide the relative file paths to our translation files.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16470 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-l10n-file-paths-1024x669.png\" alt=\"Entering the paths to our translation files to connect our app to the Phrase project | Phrase\" width=\"1024\" height=\"669\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-l10n-file-paths-1024x669.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-l10n-file-paths-300x196.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-l10n-file-paths-768x502.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-l10n-file-paths-1536x1004.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/cli-l10n-file-paths-2048x1339.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/>At this point the Phrase CLI will create a <code>.phrase.yml<\/code> config file that connects our app to the Phrase project, and will ask us if we want to perform an upload (push) of our translation files up to Phrase. Let\u2019s do so by entering <code>y<\/code> and pressing <em>Enter<\/em>.<br \/>\nOur translations should now be up on the Phrase console, and we can see them if we navigate to <em>Projects \u279e flutter-handiraft \u279e Languages<\/em> and then click on a language.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16471 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translations-after-push-1024x820.png\" alt=\"The Phrase translation interface | Phrase\" width=\"1024\" height=\"820\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translations-after-push-1024x820.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translations-after-push-300x240.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translations-after-push-768x615.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translations-after-push-1536x1229.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translations-after-push-2048x1639.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<h2><span class=\"ez-toc-section\" id=\"the-translator-experience\"><\/span>The translator experience<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>At this point, the power of a software localization platform should start becoming apparent. Our translators can utilize the Phrase web interface\u2014they can search, filter, add, remove, update, see changes, verify\/unverify, and even do team management using a job assignment interface\u2014all while our developers are busy working on core e-commerce functionality for our customers. We\u2019ve saved countless design and engineering hours by not rolling our own console for translators, and given translators a platform that is designed and built for <em>their<\/em> workflows.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"connecting-developers-and-translators\"><\/span>Connecting developers and translators<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>When developers add a new feature, they just have to <code>phrase push<\/code> their new translation keys from the command line. The translators take it from there, and once their translations are polished and ready, they can notify the developers, who perform a <code>phrase pull<\/code> and get back to writing the creative code they love. No need for exchanging and manually merging translation files. In fact, because the Phrase CLI supports all major operating systems, we can automate to our heart\u2019s content. From an engineering perspective, localization becomes a simple step in our DevOps flow.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"github-sync-and-continuous-localization\"><\/span>GitHub sync and continuous localization<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>As developers, we\u2019re used to tight, cyclical build cycles that aim for continuous integration and continuous delivery. Localization can seamlessly be part of this through repository syncing: when we push a commit to a certain branch, Phrase can react by refreshing its translations, and our translators can get to localizing immediately. Let\u2019s set this up for HandiRaft.<\/p>\n<p>\u270b\ud83c\udffd <em>Heads up \u00bb<\/em> We need to connect our apps to Phrase with <code>.phrase.yml<\/code> files in our Flutter and React apps to make GitHub sync work. The <code>phrase init<\/code> command we ran earlier took care of this for us.<\/p>\n<p>\ud83d\uddd2 <em>Note \u00bb<\/em> We\u2019re covering GitHub sync here, but Phrase can <a href=\"https:\/\/phrase.com\/roles\/developers\/\">sync with Bitbucket and GitLab<\/a> repos as well.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"generating-a-github-access-token\"><\/span>Generating a GitHub access token<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The first thing we need to do to connect GitHub to our Phrase projects is generate an access token from GitHub. Once we\u2019ve logged into our GitHub account, we can click our profile picture near the top-right of the screen and go to <em>Settings \u279e Developer settings \u279e Personal access tokens \u279e Generate new token<\/em>. This will open the <em>New personal access token screen<\/em>, and we can get our spicy new token from there.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16472 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-gen-token-1024x779.png\" alt=\"Generating a GitHub personal access token | Phrase\" width=\"1024\" height=\"779\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-gen-token-1024x779.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-gen-token-300x228.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-gen-token-768x585.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-gen-token-1536x1169.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-gen-token-2048x1559.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/>\ud83d\uddd2 <em>Note \u00bb<\/em> If our project repos are private, we\u2019ll need the entire <em>repo<\/em> scope for our token. If the repos are public, however, we just need the <em>public_repo<\/em> scope.<\/p>\n<p>Clicking the <em>Generate token<\/em> button will give us a token we can copy to a safe place. Said token in hand (or in clipboard), we can head back to Phrase, log in, and head to <em>Projects \u279e flutter-handicraft \u279e Project settings \u279e GitHub Sync<\/em>.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16473 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-enable-github-sync-881x1024.png\" alt=\"Adding GitHub sync to our Phrase project | Phrase\" width=\"881\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-enable-github-sync-881x1024.png 881w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-enable-github-sync-258x300.png 258w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-enable-github-sync-768x892.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-enable-github-sync-1322x1536.png 1322w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-enable-github-sync.png 1494w\" sizes=\"(max-width: 881px) 100vw, 881px\" \/>After providing our GitHub access token, selecting our repo and branch, validating our <code>.phrase.yml<\/code> config via the <em>Validate Configuration<\/em> button, and clicking <em>Save<\/em>, we\u2019re ready to sync our Phrase translations with our GitHub repo.<\/p>\n<p>By default, our translators would have to manually pull translation updates from our GitHub repo by going to <em>Languages \u279e GitHub Sync \u279e Import from GitHub<\/em>.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16474 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-import-from-github-1024x447.png\" alt=\"Manually importing from GitHub | Phrase\" width=\"1024\" height=\"447\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-import-from-github-1024x447.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-import-from-github-300x131.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-import-from-github-768x335.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-import-from-github-1536x670.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-import-from-github-2048x893.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<h3><span class=\"ez-toc-section\" id=\"auto-importing-using-a-webhook\"><\/span>Auto-importing using a webhook<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Manual import can be exactly what your team needs. However, we can automate the import by adding a webhook to our GitHub repo that triggers whenever our chosen branch gets a new commit pushed to it. The webhook can then automatically import translations from our repo to our Phrase project.<\/p>\n<p>To enable auto-import, we need to head back to our <em>Project settings \u279e GitHub Sync<\/em>, then check the <em>Enable auto-import from GitHub<\/em>. This will reveal a <em>Generate payload URL<\/em> button worthy of good clicking.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16475 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-enable-github-auto-import-844x1024.png\" alt=\"Enabling GitHub auto import | Phrase\" width=\"844\" height=\"1024\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-enable-github-auto-import-844x1024.png 844w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-enable-github-auto-import-247x300.png 247w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-enable-github-auto-import-768x932.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-enable-github-auto-import-1266x1536.png 1266w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-enable-github-auto-import.png 1512w\" sizes=\"(max-width: 844px) 100vw, 844px\" \/>A <em>Payload URL<\/em> is revealed, which we can copy to a safe place. Let\u2019s click <em>Save<\/em> and make our way to GitHub to set up the auto-import webhook.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16476 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-auto-import-webhook-payload-1024x487.png\" alt=\"Generating a GitHub webhook payload | Phrase\" width=\"1024\" height=\"487\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-auto-import-webhook-payload-1024x487.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-auto-import-webhook-payload-300x143.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-auto-import-webhook-payload-768x365.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-auto-import-webhook-payload.png 1498w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/>From our GitHub repo\u2019s home page, let\u2019s navigate to <em>Settings \u279e Webhooks<\/em> and click <em>Add webhook<\/em>.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16477 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-add-webhook-1024x669.png\" alt=\"Adding webhook to GitHub repo | Phrase\" width=\"1024\" height=\"669\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-add-webhook-1024x669.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-add-webhook-300x196.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-add-webhook-768x502.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-add-webhook-1536x1004.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-add-webhook-2048x1338.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/>We just need to paste our <em>Payload URL<\/em> in its namesake field and make sure the <em>Content type<\/em> is set to <em>application\/json<\/em>. Leaving all other fields as they are, we can click <em>Add webhook<\/em>, and we\u2019re set.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"continuous-integration-continuous-localization\"><\/span>Continuous integration, continuous localization<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>With auto importing in place, we can now simply work in our normal Git flow and ensure that our translators get the latest translation keys as soon as they\u2019re ready for them to translate. From an engineering perspective, translation becomes part of our continuous integration flow:<\/p>\n<ol>\n<li>We work on a new feature, adding and updating translations in the source language (say English).<\/li>\n<li>As soon as the feature starts taking shape, we push a commit to the branch that we registered with the Phrase projects.<\/li>\n<li>Translators automatically receive the new translation keys in their Phrase project and use Phrase to efficiently translate our new feature to all our supported locales.<\/li>\n<li>When they\u2019re done, translators export a pull request (PR) to GitHub, which we can review and merge.<\/li>\n<\/ol>\n<p>It\u2019s really that easy. Let\u2019s see it in action. Let\u2019s say we\u2019re adding a social forum section to our offering, where pro and hobbyist artisans can talk about their craft. We\u2019ll have some new strings in our new screens, of course. While we develop, we add these strings to our development language, English.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-linenumbers=\"false\" data-enlighter-group=\"3871dce2-a52f-42ff-b158-c4d902fcc65b\" data-enlighter-title=\"lib\/l10n\/app_en.arb\">{\r\n  \/\/ ...\r\n  \"artisanChat\": \"Artisan chat\",\r\n  \"createPost\": \"Create a post\",\r\n  \"publish\": \"Publish\",\r\n  \"rulesOfConduct\": \"Rules of conduct\"\r\n}\r\n<\/pre>\n<p>We feel that the forum is heading in the right direction, and we want to get our localization team working on it ASAP. So we simply push a commit to the branch connected to our Phrase project, <code>main<\/code> in this case.<\/p>\n<p>As soon as we do, our new translations are available in Phrase.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16478 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translations-after-github-push-1024x816.png\" alt=\"Translations appear in Phrase immediately after our Git push | Phrase\" width=\"1024\" height=\"816\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translations-after-github-push-1024x816.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translations-after-github-push-300x239.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translations-after-github-push-768x612.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translations-after-github-push-1536x1224.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translations-after-github-push-2048x1632.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/>Our translators take over now, utilizing all the power of Phrase to manage and translate the new strings into all the locales we support. Our translators could have dozens of languages to manage here, and they\u2019re using Phrase&#8217;s translation features to take care of that. We\u2019re busy plugging away at our feature code. Once their translations are ready, they just need to export a PR for us to look at.<\/p>\n<p>This is easily done on the Phrase console by going to <em>Project \u279e Languages \u279e GitHub Sync \u279e Export to GitHub as pull request<\/em>.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16479 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-export-pr-1024x787.png\" alt=\"Exporting updated localizations from Phrase as a GitHub PR | Phrase\" width=\"1024\" height=\"787\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-export-pr-1024x787.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-export-pr-300x231.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-export-pr-768x591.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-export-pr-1536x1181.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-export-pr-2048x1575.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/>The PR immediately appears in our GitHub repo.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16480 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-pr-from-phrase-export-1024x548.png\" alt=\"The new PR appearing in GitHub | Phrase\" width=\"1024\" height=\"548\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-pr-from-phrase-export-1024x548.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-pr-from-phrase-export-300x161.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-pr-from-phrase-export-768x411.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-pr-from-phrase-export-1536x822.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-pr-from-phrase-export.png 1848w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16481 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-pr-diff-1024x662.png\" alt=\"The PR diff showing the updated translations | Phrase\" width=\"1024\" height=\"662\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-pr-diff-1024x662.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-pr-diff-300x194.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-pr-diff-768x496.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-pr-diff-1536x993.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/github-pr-diff-2048x1324.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/div>\n<p>Just like any other PR, we can review and merge it in, making the new translations available to our whole team without us doing anything outside of our normal workflow: Git push, PR, review, merge. Presto.<\/p>\n<p>So Phrase Strings saves our engineering team many precious hours, and headaches, by automating localization integration.<\/p>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16627\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/connecting-to-phrase.jpg\" alt=\"Connecting to Phrase for localization | Phrase\" width=\"500\" height=\"500\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/connecting-to-phrase.jpg 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/connecting-to-phrase-300x300.jpg 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/connecting-to-phrase-150x150.jpg 150w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/connecting-to-phrase-768x768.jpg 768w\" sizes=\"(max-width: 500px) 100vw, 500px\" \/><\/p>\n<div>\ud83d\udd17 <em>Resource \u00bb<\/em> You can get the source code for our mocked-up apps from the companion GitHub repos: the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/flutter-i18n-at-scale\">Flutter app repo<\/a> and the <a href=\"https:\/\/github.com\/PhraseApp-Blog\/react-i18n-at-scale-2022\">React app repo<\/a>.<\/div>\n<h2><span class=\"ez-toc-section\" id=\"saving-time-for-translators-translation-memory\"><\/span>Saving time for translators: translation memory<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>This tutorial is aimed at developers, but we do want to briefly touch on a few features in Phrase Strings that save translators time.<\/p>\n<p>One issue translators often face is duplicate translations, especially across multiple apps in the same offering. For example, the HandiRaft Flutter and React apps share a lot of the same translation keys.<\/p>\n<p>We can dramatically reduce the amount of effort around duplicate translations across apps by enabling Phrase\u2019s t<em>ranslation memory<\/em>. After we log into Phrase, we can find it in the sidebar to the left of the screen.<\/p>\n<div><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16483 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translation-memory-1024x771.png\" alt=\"Setting up Phrase Translation Memory | Phrase\" width=\"1024\" height=\"771\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translation-memory-1024x771.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translation-memory-300x226.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translation-memory-768x578.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translation-memory-1536x1157.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-translation-memory-2048x1542.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/div>\n<p>Once on the t<em>ranslation memory<\/em> page, we just need to select the Phrase projects to connect and then click <em>Update settings<\/em>. Of course, here we\u2019ll connect our <em>flutter-handiraft<\/em> and <em>react-handiraft<\/em> projects.<\/p>\n<p>Our web team has been busy updating our React app to include the social forum feature that the Flutter team started earlier. Of course, the React app\u2019s new translation strings are very similar to ones recently added to the Flutter app.<\/p>\n<p>However, instead of spending time re-translating these strings, our translators can use the enabled t<em>ranslation memory<\/em> to get automatic <em>autocomplete <\/em>suggestions and populate their translations with one button, while reviewing them to ensure quality.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-16484 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-suggested-translation-memory-1024x777.png\" alt=\"Phrase's Translation Memory suggesting a translation across projects | Phrase\" width=\"1024\" height=\"777\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-suggested-translation-memory-1024x777.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-suggested-translation-memory-300x228.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-suggested-translation-memory-768x583.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-suggested-translation-memory-1536x1165.png 1536w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/03\/phrase-suggested-translation-memory-2048x1554.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><em>Translation memory<\/em> + <em>autocomplete<\/em> can save our translators countless hours across a project. But Phrase gives translators much more than that:<\/p>\n<ul>\n<li>Comments so that translators can collaborate with the translation right in front of them<\/li>\n<li>See changes in the translation and the ability to revert to earlier versions of a translation<\/li>\n<li>An in-context editor so translators can translate directly on the interface of web apps<\/li>\n<li><a href=\"https:\/\/phrase.com\/roles\/translators\/\">And much more<\/a><\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"wrapping-up-our-tutorial-on-how-to-localize-software-at-scale\"><\/span>Wrapping up our tutorial on how to localize software at scale<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Back to my engineering tribe, we\u2019ve covered Phrase\u2019s CLI and GitHub sync, but Phrase is built by developers for developers, so it gives us a lot more goodies:<\/p>\n<ul>\n<li>Support for almost every translation file format under the sun<\/li>\n<li>An API we can connect to<\/li>\n<li>Branching<\/li>\n<li>Over-the-air (OTA) translations for mobile apps<\/li>\n<li><a href=\"https:\/\/phrase.com\/roles\/developers\/\">And much more<\/a><\/li>\n<\/ul>\n<p>We hope you\u2019ve seen how much a platform like Phrase Strings can save your team time and money as your app scales, allowing you to focus on your core offering while leaving the heavy lifting regarding localization tech to Phrase. Are there topics that we missed here that you would like us to cover? We&#8217;d love to hear from you.<\/p>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n\n\n\n<div id=\"acf\/blog-cta-block_97941941c8dbd70dcea8e0129ecabd99\" class=\"pxblock pxblock--blog-cta bg--green image--orientation-landscape\">\n\t<div class=\"block-container\">\n\t\t\t\t\t<div class=\"image image--align-middle\">\n\t\t\t\t<img loading=\"lazy\" decoding=\"async\" width=\"1260\" height=\"992\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero.png\" class=\"attachment-original size-original\" alt=\"String Management UI visual | Phrase\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero.png 1260w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero-300x236.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero-1024x806.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2022\/08\/String_Hero-768x605.png 768w\" sizes=\"(max-width: 1260px) 100vw, 1260px\" \/>\t\t\t<\/div>\n\t\t\t\t<div class=\"content\">\n\t\t\t<p class=\"subhead\">Phrase Strings<\/p>\n<p class=\"secondary h6\">Take your web or mobile app global without any hassle<\/p>\n<div class=\"text--copy\">\n<p class=\"small\">Adapt your software, website, or video game for global audiences with the leanest and most realiable software localization platform.<\/p>\n<p><a class=\"btn btn--outline\" href=\"https:\/\/phrase.com\/platform\/strings\/\">Explore Phrase Strings<\/a><\/p>\n<\/div>\n\t\t<\/div>\n\t<\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Taking software from an MVP to a full-fledged solution? Learn how to remove roadblocks to growth and localize software for a global user base.<\/p>\n","protected":false},"author":41,"featured_media":2612,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_stopmodifiedupdate":false,"_modified_date":"","_searchwp_excluded":"","footnotes":""},"categories":[40],"class_list":["post-16169","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-software-localization"],"acf":[],"_links":{"self":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/16169"}],"collection":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/users\/41"}],"replies":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/comments?post=16169"}],"version-history":[{"count":14,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/16169\/revisions"}],"predecessor-version":[{"id":75431,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/posts\/16169\/revisions\/75431"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media\/2612"}],"wp:attachment":[{"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/media?parent=16169"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/phrase.com\/wp-json\/wp\/v2\/categories?post=16169"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}