<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>l10n Archives - Alexandros Georgiou</title>
	<atom:link href="https://www.alexgeorgiou.gr/tag/l10n/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.alexgeorgiou.gr/tag/l10n/</link>
	<description>Balancing brackets for a living</description>
	<lastBuildDate>Wed, 19 Feb 2025 12:52:40 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.alexgeorgiou.gr/wp-content/uploads/2021/07/cropped-alexgeorgiou-icon-32x32.png</url>
	<title>l10n Archives - Alexandros Georgiou</title>
	<link>https://www.alexgeorgiou.gr/tag/l10n/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>♊︎ Using Google Gemini to intelligently translate a Flutter app to many locales on Linux</title>
		<link>https://www.alexgeorgiou.gr/flutter-localization/</link>
					<comments>https://www.alexgeorgiou.gr/flutter-localization/#respond</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Wed, 19 Feb 2025 12:52:38 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[arb]]></category>
		<category><![CDATA[automation]]></category>
		<category><![CDATA[flutter]]></category>
		<category><![CDATA[flutter driver]]></category>
		<category><![CDATA[Gemini]]></category>
		<category><![CDATA[i18n]]></category>
		<category><![CDATA[l10n]]></category>
		<category><![CDATA[llm]]></category>
		<category><![CDATA[Play Console]]></category>
		<category><![CDATA[screenshot]]></category>
		<category><![CDATA[translation]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=1815</guid>

					<description><![CDATA[<p>I have created a collection of assorted scripts to translate my flutter apps to multiple languages. Here I share some of this magic with the world.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/flutter-localization/">♊︎ Using Google Gemini to intelligently translate a Flutter app to many locales on Linux</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Flutter enables the rapid development of apps primarily targeting the Google Play store and the Apple App Store. Competition on these stores is fierce, and you need every bit of edge that you can have against your competitors.</p>



<p>One edge that your app can have, is translation to multiple languages.</p>



<p>You can potentially provide string translations for over 70 languages, but most app developers only choose to provide translations for a few of the most popular languages. The Play Console for example offers machine translation for 12 languages.</p>



<p>You can do better. You don&#8217;t need to miss out on the long tail of people speaking languages other than Spanish, French, German, etc. There is actually very little cost to provide localisations for more languages, provided you do it right.</p>



<p>Localizing an app involves two things: <strong>Localizing the strings in the app</strong>, and <strong>localizing the app store presence</strong>.</p>



<h2 class="wp-block-heading">Localizing the app strings</h2>



<p>App internationalization/localization in flutter is explained in this very thorough, authoritative guide:</p>



<p><a href="https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization" target="_blank" rel="noreferrer noopener">https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization</a></p>



<p>After following the guide, you likely have your English strings in the JSON file <code>lib/l10n/app_en.arb</code>.</p>



<p>At this point, unless you have access to a professional translation service powered by humans, you are likely thinking of translating these <code>.arb</code> files using Google translate or some other automated translation tool. I am here to tell you that this is not ideal.</p>



<p>Automated translation tools often produce poor translations because each natural language has its nuisances. Something that can only have one word in the English language can correspond to multiple different words in another language, depending on the context. Automated translations often fail and produce awkward or clumsy output because they lack this valuable context. Using this context requires knowledge about the world, and for that you need AI.</p>



<p>Enter LLMs. It is my firm belief that it&#8217;s better to use an LLM to translate app strings. The reason is that LLMs know about context, and you as a developer can provide more context about your app. If the LLM knows that it&#8217;s translating strings for an app, and it knows what the app is, it will provide translations of better quality than typical machine translations.</p>



<p>Here is the complete code I use to translate the app strings and to generate screenshots:</p>



<p><a href="https://gist.github.com/alex-georgiou/48430da5b31501de6f8e58796b6183fe" target="_blank" rel="noreferrer noopener">https://gist.github.com/alex-georgiou/48430da5b31501de6f8e58796b6183fe</a></p>



<h3 class="wp-block-heading">Translating <code>.arb</code>s using a script that calls Gemini</h3>



<p>What I do is place the following two files in the root of my flutter project: <code>make_arb_files.sh</code> and <code>make_arb_files.csv</code>.</p>



<p>Then I make the shell script executable with:</p>



<pre class="wp-block-code"><code>chmod +x make_arb_files.sh</code></pre>



<p>Then I enter my <a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noreferrer noopener">Gemini API key</a> into a file named <code>gemini_key</code>.</p>



<p>Having done this, I proceed to edit the prompt template in the shell script. I provide some context about the app, i.e. what it is and what it does.</p>



<p>My advice is to be somewhat verbose here, as this is the valuable context that is going to make the difference in translation quality, with respect to a plain-old auto translation.</p>



<p>Then I run the script and it reads the app_en.arb file, and it generates any missing <code>.arb</code> files. If I need to regenerate a file, I delete it and then I run the script again.</p>



<h3 class="wp-block-heading">It&#8217;s free!</h3>



<p>You can run this script a few times per day without incurring any costs. Google gives you about 1000 free queries per day, and each of the 70 languages or so is one query.</p>



<h2 class="wp-block-heading">Localizing the app store presence</h2>



<p>The approach that I&#8217;m using is to keep the icon of the app static across all languages, and to translate the following:</p>



<ul class="wp-block-list">
<li>App title</li>



<li>App description</li>



<li>Store banner</li>



<li>Screenshots</li>
</ul>



<p>I do not bother with translating the release notes / changelog, since no-one ever reads these!</p>



<h3 class="wp-block-heading">App title and description</h3>



<p>I run another script once to translate the app title and description to all the languages, then enter the texts into the Play store. Unfortunately this has to be done manually.</p>



<h3 class="wp-block-heading">Store banner</h3>



<p>For the banner, I use an image that features some text, and this is again translated using a script, but here there is a complication: The text font usually needs to be adjusted to fit the banner size. Using trial and error, you can figure out the correct font size for each language. Once I have a translated set of PNG banners, I upload these to the app store as well.</p>



<p>I won&#8217;t provide code for this here, because YMMV. But you can create SVG files based on a template and using the translated titles/descriptions, then convert the SVG files to PNG using <a href="https://imagemagick.org/">ImageMagick</a>.</p>



<h3 class="wp-block-heading">Screenshots</h3>



<p>For the screenshots, the process is somewhat harder. To automate taking screenshots for multiple languages, and for multiple device types (Mobile, 7&#8243; tablets and 10&#8243; tablets), you need to create a test driver as follows:</p>



<p>First, copy the file <code>integration_test.dart</code> into the <code>test_driver</code> directory. This tells your test script how to take a PNG screenshot. The screenshot will be saved to the path specified in the code, and you may want to change this.</p>



<p>Then, copy the <code>screenshots_test.dart</code> file into the <code>test</code> directory. Here you must use the driver to load the app and interact with it so as to arrive at a screen that you want to save. Each test is a separate run of the app from scratch, and should finish with writing a screenshot. You can only take one screenshot per test.</p>



<p>Finally, copy the <code>make_screenshots.sh</code> shell file into your project&#8217;s root, and make it executable with <code>chmod +x</code>. This script loops over all languages and runs the <code>screenshots_test.dart</code> tests in the currently running AVD, thus effectively creating the screenshots. The tests load the app with <code>debugShowCheckedModeBanner: false</code>, because you don&#8217;t want a red &#8220;Debug&#8221; ribbon on the top right part of the screenshot.</p>



<h4 class="wp-block-heading">Generating screenshots for multiple device types</h4>



<p>Now start your AVD (e.g. Pixel 7 for mobile phone screenshots), and run the script. The script will take ages to complete, but should not require any more intervention on your part. It will go ahead and create all screenshots for Pixel 7!</p>



<p>Once finished, stop the AVD and start a Nexus 7 AVD. Repeat the process to generate screenshots for 7&#8243; tablets. Then, do the same for 10&#8243; tablets (Nexus 10).</p>



<p>Importantly, optimize the PNG files to reduce their size. I use <a href="https://trimage.org/" target="_blank" rel="noreferrer noopener">trimage</a>, but any PNG optimizer will do. This step is important, because as you are uploading the screenshots into Google Play, the UI of the Play console will get progressively slower and slower, since it holds all the images in memory for all devices and all languages! Having smaller files and more system memory helps a lot here.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p>None of this is easy, and it requires some patience, but hopefully with the code I provide here, it is somewhat easier.</p>



<p>I believe the extra effort is worth it: Do not underestimate the power of having many, high-quality translations to your app. Non-English speakers will appreciate your app a lot when they see their obscure native language in the store. The translated store presence (title, description, banner, screenshots) is what will make them decide to use your app over someone else&#8217;s.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/flutter-localization/">♊︎ Using Google Gemini to intelligently translate a Flutter app to many locales on Linux</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/flutter-localization/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>😛 How to make pot with WordPress</title>
		<link>https://www.alexgeorgiou.gr/make-pot-wordpress/</link>
					<comments>https://www.alexgeorgiou.gr/make-pot-wordpress/#respond</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Thu, 29 Mar 2018 18:08:46 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[grunt]]></category>
		<category><![CDATA[grunt-po2mo]]></category>
		<category><![CDATA[grunt-pot]]></category>
		<category><![CDATA[grunt-wp-i18n]]></category>
		<category><![CDATA[i18n]]></category>
		<category><![CDATA[l10n]]></category>
		<category><![CDATA[plugin]]></category>
		<category><![CDATA[pot]]></category>
		<category><![CDATA[translation]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=241</guid>

					<description><![CDATA[<p>Build translation files to localize a WordPress plugin using grunt. Create separate translation files for the front-end and back-end.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/make-pot-wordpress/">😛 How to make pot with WordPress</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>In this article I will detail how to use grunt to build translation files to <a href="https://developer.wordpress.org/plugins/internationalization/localization/" target="_blank" rel="noopener">localize a WordPress plugin</a>. We&#8217;ll create separate translation files for the front-end and back-end (admin area), potentially allowing a translator to translate only one or the other.</em></p>
<h2>Have you tried POT?</h2>
<p>Hopefully you&#8217;ve already used the i18n functions in your PHP code. For example, a simple translatable string would look like:</p>
<pre>$text = <a href="https://developer.wordpress.org/reference/functions/__/" target="_blank" rel="noopener">__</a>( 'Text to internationalize', 'language-domain-slug' );</pre>
<p>The purpose of this article is not to list all the relevant functions, you can look them up <a href="https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/" target="_blank" rel="noopener">here</a>. These are all wrappers to <a href="https://www.gnu.org/software/gettext/" target="_blank" rel="noopener">gettext</a>.</p>
<p>The important thing to note is that you need to run something to generate your <em>Portable Object Template</em> files. These <code>.pot</code> files can then be copied to <em>Portable Object</em> <code>.po</code> files, then translated to <em>Machine Object</em> <code>.mo</code> files. These <code>.mo</code> binaries are finally loaded by the end user&#8217;s WordPress installation to provide the translated strings to the plugin, or theme. No surprises up to here, all of this is standard.</p>
<h2>Grow your own with Grunt!</h2>
<p>What I will present today is an easy way to use <a href="https://gruntjs.com" target="_blank" rel="noopener">grunt</a> to generate the following:</p>
<ol>
<li>Two <code>.pot</code> files, one with the front-end strings and one with the admin area strings, and</li>
<li><code>.mo</code> files for any <code>.po</code> files in your project</li>
</ol>
<p>You will still need to copy the <code>.pot</code> files to <code>.po</code> files manually and provide the translations. Let&#8217;s get started:</p>
<p>The first thing to add to our <code>Gruntfile.js</code> is <a href="https://www.npmjs.com/package/grunt-wp-i18n" target="_blank" rel="noopener">grunt-wp-i18n</a>. This is a grunt plugin that I have found to be a little more flexible than the alternative, <a href="https://www.npmjs.com/package/grunt-pot" target="_blank" rel="noopener">grunt-pot</a>. So, as usual, add this grunt plugin to your dev toolbox:</p>
<div class="highlight shell">
<pre class="line"><span class="source shell">npm install grunt-wp-i18n --save-dev</span></pre>
<div class="line">and then add the following into your Gruntfile, to get the plugin loaded:</div>
<div class="line">
<div class="highlight js">
<pre class="line"><span class="source js"><span class="variable other object js">grunt</span><span class="meta js"><span class="meta delimiter method period js">.</span><span class="entity name function js">loadNpmTasks</span><span class="punctuation definition begin round js">(</span> <span class="string quoted single js"><span class="punctuation definition string begin js">'</span>grunt-wp-i18n<span class="punctuation definition string end js">'</span></span> <span class="punctuation definition end round js">)</span></span><span class="punctuation terminator statement js">;</span></span></pre>
<div class="line">Now we need to configure this build task. We will hide our pot in our usual stash, the <code>/languages</code> directory of our WordPress plugin. I like to keep my source code in version control under <code>/src</code> and copy it over to assemble the build under <code>/build</code>. I keep any <code>.po</code> translations under <code>/src/languages</code>, and auto-generate <code>.pot</code> files into the <code>/build/languages</code> dir. Any <code>.po</code> files are simply copied from the <code>/src</code> to the <code>/build</code> directory. This is a good practice that keeps our pot always fresh!</div>
<pre class="line">makepot: {
    front: {
        options: {
            cwd: 'build',
            potFilename: 'myplugin-front.pot',
            include: [ '/path/to/files', '/more/files', '/etc' ],
            domainPath: 'languages/',
            type: 'wp-plugin'
        }
    },
    admin: {
        options: {
            cwd: 'build',
            potFilename: 'myplugin.pot',
            include: [ '.*' ],
            exclude: [ '/path/to/files', '/more/files', '/etc' ],
            domainPath: 'languages/',
            type: 'wp-plugin'
    }
}</pre>
<div class="line">Notice that in this example there are two files: The <code>myplugin-front.pot</code> file pulls strings from a list of files that make up the front-end, while the <code>myplugin.pot</code> pulls strings from every other thing in my plugin, which would be the admin area.</div>
<blockquote>
<div class="line">Now a translator is free to only translate the front-end, leaving the back-end, which may contain a lot more strings, to its default (English?) language.</div>
</blockquote>
<div class="line">In functions such as <a href="https://developer.wordpress.org/reference/functions/__/" target="_blank" rel="noopener">__()</a>, the second argument is a <em>language domain</em>. People usually set here the plugin&#8217;s slug, but this is not strictly necessary: You can use different domains for e.g. the front-end and the back-end. Here we will use <code>myplugin-front</code> for the front-end and <code>myplugin</code> for the back-end. This has to be done in the PHP code, wherever the i18n functions are used.</div>
</div>
</div>
</div>
<h2 class="line">Make pot</h2>
<div class="line">If all is set up correctly, you can now easily make pot with:</div>
<pre class="line">grunt makepot</pre>
<p class="line">This will make two files in your project, <code>/build/languages/myplugin.pot</code> and <code>/build/languages/myplugin-frontend.pot</code>. Do not commit these files, we&#8217;ll let them auto-generate every time. The <code>/build</code> directory is in my <code>.gitignore</code> list.</p>
<h2 class="line">Using pot</h2>
<div class="line">For your plugin to actually use this, you need to copy the <code>.pot</code> files into <code>.po</code> files. Depending on your language, you will have to use the correct <em>language code</em> in your filename. See the <a href="https://www.gnu.org/software/gettext/manual/gettext.html#Language-Codes" target="_blank" rel="noopener">gettext list of language codes</a> or simply the <a href="https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes" target="_blank" rel="noopener">Wikipedia list of ISO 639-1 language codes</a>. For instance, to do a German translation in our example, you would need to copy the following:</div>
<pre class="line">cp build/languages/myplugin-front.pot src/languages/myplugin-front-de_DE.po
cp build/languages/myplugin.pot src/languages/myplugin-de_DE.po</pre>
<div class="line">or to do a translation to Arabic:</div>
<div class="line">
<pre class="line">cp build/languages/myplugin-front.pot src/languages/myplugin-front-ar.po
cp build/languages/myplugin.pot src/languages/myplugin-ar.po</pre>
<div class="line">You will need to check to make sure whether you need to use a <em>country code</em> along with your language code.</div>
<div class="line">Once you have your <code>.po</code> files you can load them up in <a href="https://poedit.net/" target="_blank" rel="noopener">poedit</a> and start translating. Once the translations are ready you can commit them to your source tree.</div>
<h2 class="line">Packaging it all together for shipment</h2>
<div class="line">WordPress will not use your <code>.pot</code> or <code>.po</code> files. You only include these in your plugin so as to help future translators. To actually enable translation you need to convert the <code>.po</code> files to <code>.mo</code> files.</div>
</div>
<p>We&#8217;ll use another grunt plugin, <a href="https://www.npmjs.com/package/grunt-po2mo" target="_blank" rel="noopener">grunt-po2mo</a>. First add it to your project:</p>
<div class="highlight shell">
<pre class="line"><span class="source shell">npm install grunt-po2mo --save-dev</span></pre>
</div>
<p>And add the following into your Gruntfile to load it:</p>
<div class="highlight js">
<pre class="line"><span class="source js"><span class="variable other object js">grunt</span><span class="meta js"><span class="meta delimiter method period js">.</span><span class="entity name function js">loadNpmTasks</span><span class="punctuation definition begin round js">( </span><span class="string quoted single js"><span class="punctuation definition string begin js">'</span>grunt-po2mo<span class="punctuation definition string end js">' </span></span><span class="punctuation definition end round js">)</span></span><span class="punctuation terminator statement js">;</span></span></pre>
</div>
<p>This one requires very little configuration. Add the following target into your Gruntfile:</p>
<pre>po2mo: {
    plugin: {
        cwd: 'src',
        src: 'languages/*.po',
        dest: 'build',
        expand: true
    }
},</pre>
<p>This config expects that the <code>.po</code> files are in the <code>/src/languages</code> directory where we placed them earlier. It places the <code>.mo</code> files in the <code>/build/languages</code> dir. All you need to do for this is:</p>
<pre>grunt po2mo</pre>
<p>You will also need to tell your plugin to actually go and use these <code>.mo</code> files. Easy. Just hook to the <code>plugins_loaded</code> action and load the text domains:</p>
<pre>function action_plugins_loaded() {
    load_plugin_textdomain( 'myplugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
    load_plugin_textdomain( 'myplugin-front', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
} // end function action_plugins_loaded

add_action( 'plugins_loaded', 'action_plugins_loaded' );</pre>
<h2>That&#8217;s it!</h2>
<p>Make sure to include the <code>makepot</code> and <code>po2mo</code> targets in your build task. Finally use <code>grunt-contrib-compress</code> or another plugin to compress your entire build directory to a <code>.zip</code> file that is ready to be installed into WordPress!</p>
<p>Pass it on.</p>
<p>&nbsp;</p>
<p>The post <a href="https://www.alexgeorgiou.gr/make-pot-wordpress/">😛 How to make pot with WordPress</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/make-pot-wordpress/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
