Any developer who has encountered the challenge of localization at least once will tell you that it's a tedious job. I think we can agree that taking care of multiple language versions just isn't as fun as introducing new features to the product you love. And we are not talking only about internationalization but also about managing translators, handling different versions, translation ping-pong, and the countless revisions. That's why localization is often neglected and buried deep in the backlog.
What if we told you that you could completely automate the localization process with Localazy and GitHub Actions? And by completely, we mean completely.
What's Localazy? Localazy is a complete localization suite built with developers in mind. Localazy allows you to fully automate the localization of your Laravel projects. Set it up once and forget about the hassle forever.
You will learn how to:
- install the Laravel Framework and run your first application,
- set up your Git repository,
- prepare your Laravel project for localization,
- connect it with Localazy and translate it into multiple languages,
- automate string uploads and translation downloads with GitHub Actions.
Install Laravel framework
There are a couple of ways to install Laravel (choose the one that suits you and your OS). Typically, you'd probably use composer to install the framework. But as I use macOS and want to keep my laptop as clean as possible, I chose to install Laravel via curl, which downloads a containerized application.
This is a huge advantage if you're developing on a Mac and have Docker Desktop up and running - and we know the benefits of dockerized applications (for everyone interested, I recommend reading Docker Deep Dive: Zero to Docker in a single book).
Let's install the application. I typed laravel-i18n-gh-actions-example
as the name of my app.
<code data-theme="github-light" data-lang="bash" class="torchlight" readability="2"><p><span class="line-number">1</span><span>curl</span><span> </span><span>-s</span><span> </span><span>"https://laravel.build/laravel-i18n-gh-actions-example"</span><span> </span><span>|</span><span> </span><span>bash</span></p></code>
Installing Laravel via curl
The application is shipped with Laravel Sail, which was introduced with Laravel 8. It's a command-line interface for interacting with Laravel's default Docker development environment. Sail provides a way for building a Laravel application without requiring prior Docker experience.
Run the Laravel application
Alright, the project is created. It's time to run the application. Navigate to the application directory and start Laravel Sail.
<code data-theme="github-light" data-lang="bash" class="torchlight" readability="2"><p><span class="line-number">1</span><span>cd</span><span> </span><span>laravel-i18n-gh-actions-example</span><span> && </span><span>./vendor/bin/sail</span><span> </span><span>up</span></p></code>
Running the Laravel Sail
This process runs the application. It can take a while for the first time as application containers need to be built, so be patient. It's good to note - if you're a Docker expert - everything about Sail can be customized using the docker-compose.yml
file included with Laravel.
After the application's Docker containers have been started, you can access the application in your web browser at: http://localhost
. You should see a screen similar to the one below.
Laravel application up and running
Setup Git - what do we want to achieve?
In the following steps, we'll prepare our git repository for the workflow we'll set up later. As you can imagine, there are dozens of workflows suitable for different types of apps - it all depends on your needs.
I'd like to show you a relatively simple example so that you can understand the GitHub Actions. The following steps won't make much sense if you don't know what I want to achieve. So what is it?
Imagine this workflow:
- We have two main branches,
develop
andmaster
, - then, for every task we work on, we create a new branch (depending on the task title - name it
foo
for our foo simple task), - in
foo
branch, we define new source keys as we work on the task, - when it's ready, we create a pull request to
develop
, - at this point, we want the source keys to be uploaded & synced to Localazy for translation,
- meanwhile, translators can work on translations,
- then, when it's time to release the app, we will create a pull request from
develop
tomaster
, - now, when we accept the PR and therefore push to
master
branch, we want to download the translations (in localization files) and push them to master with the code, and most likely run some other tasks (like test the app, build/ship the app, ...) - depending on your needs, - then, everything is ready.
Create & initialize the Git repository
Now we need to set up a Git. Go to your GitHub and create an empty repository. Copy the remote address and init git in our Laravel project.
<code data-theme="github-light" data-lang="bash" class="torchlight"><p><span class="line-number">1</span><span>git</span><span> </span><span>init</span></p></code>
Then, add a new remote and paste the copied address.
<code data-theme="github-light" data-lang="bash" class="torchlight" readability="2"><p><span class="line-number">1</span><span>git</span><span> </span><span>remote</span><span> </span><span>add</span><span> </span><span>origin</span><span> </span><span>git@github.com:localazy/laravel-i18n-gh-actions-example.git</span></p></code>
Let's push the project to master
branch. I use VSCode, so I've done it all in the user interface as it's more convenient, at least for me.
Then, create a develop
branch and switch to it. Publish the branch to remote.
<code data-theme="github-light" data-lang="bash" class="torchlight"><p><span class="line-number">1</span><span>git</span><span> </span><span>switch</span><span> </span><span>-c</span><span> </span><span>develop</span></p></code>
Now, create a foo
branch and switch there. Our Laravel-related code things will be happening here. We'll get there in a moment.
<code data-theme="github-light" data-lang="bash" class="torchlight"><p><span class="line-number">1</span><span>git</span><span> </span><span>switch</span><span> </span><span>-c</span><span> </span><span>foo</span></p></code>
Prepare Blade templates & source translation file(s)
We're in the foo
branch, it's time to prepare the Blade template for localization. There are two main approaches to localizing Laravel applications. One uses PHP files, and the second one uses JSON files. You can also combine them both together, which might also be a use case in your project.
Anyway, in this example, we're going to use php
files. Translation files are located in the lang
directory in the application root. As our source language is English, create a new file in the en
directory called welcome.php
.
<code data-theme="github-light" data-lang="php" class="torchlight" readability="65"><p><span class="line-number"> 1</span><span>return</span><span> [</span></p><p><span class="line-number"> 2</span><span> </span><span>'laravel'</span><span> </span><span>=></span><span> </span><span>'Laravel'</span><span>,</span></p><p><span class="line-number"> 3</span><span> </span><span>'home'</span><span> </span><span>=></span><span> </span><span>'Home'</span><span>,</span></p><p><span class="line-number"> 4</span><span> </span><span>'log_in'</span><span> </span><span>=></span><span> </span><span>'Log in'</span><span>,</span></p><p><span class="line-number"> 5</span><span> </span><span>'register'</span><span> </span><span>=></span><span> </span><span>'Register'</span><span>,</span></p><p><span class="line-number"> 6</span><span> </span><span>'documentation'</span><span> </span><span>=></span><span> </span><span>'Documentation'</span><span>,</span></p><p><span class="line-number"> 7</span><span> </span><span>'documentation_text'</span><span> </span><span>=></span><span> </span><span>'Laravel has wonderful, thorough documentation covering every aspect of the framework. Whether you are new to the framework or have previous experience with Laravel, we recommend reading all of the documentation from beginning to end.'</span><span>,</span></p><p><span class="line-number"> 8</span><span> </span><span>'laracasts'</span><span> </span><span>=></span><span> </span><span>'Laracasts'</span><span>,</span></p><p><span class="line-number"> 9</span><span> </span><span>'laracasts_text'</span><span> </span><span>=></span><span> </span><span>'Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.'</span><span>,</span></p><p><span class="line-number">10</span><span> </span><span>'laravel_news'</span><span> </span><span>=></span><span> </span><span>'Laravel News'</span><span>,</span></p><p><span class="line-number">11</span><span> </span><span>'laravel_news_text'</span><span> </span><span>=></span><span> </span><span>'Laravel News is a community driven portal and newsletter aggregating all of the latest and most important news in the Laravel ecosystem, including new package releases and tutorials.'</span><span>,</span></p><p><span class="line-number">12</span><span> </span><span>'vibrant_ecosystem'</span><span> </span><span>=></span><span> </span><span>'Vibrant Ecosystem'</span><span>,</span></p><p><span class="line-number">13</span><span> </span><span>'vibrant_ecosystem_text'</span><span> </span><span>=></span><span> </span><span>'Laravel</span><span>\'</span><span>s robust library of first-party tools and libraries, such as <a href="https://forge.laravel.com" class="underline">Forge</a>, <a href="https://vapor.laravel.com" class="underline">Vapor</a>, <a href="https://nova.laravel.com" class="underline">Nova</a>, and <a href="https://envoyer.io" class="underline">Envoyer</a> help you take your projects to the next level. Pair them with powerful open source libraries like <a href="https://laravel.com/docs/billing" class="underline">Cashier</a>, <a href="https://laravel.com/docs/dusk" class="underline">Dusk</a>, <a href="https://laravel.com/docs/broadcasting" class="underline">Echo</a>, <a href="https://laravel.com/docs/horizon" class="underline">Horizon</a>, <a href="https://laravel.com/docs/sanctum" class="underline">Sanctum</a>, <a href="https://laravel.com/docs/telescope" class="underline">Telescope</a>, and more.'</span><span>,</span></p><p><span class="line-number">14</span><span> </span><span>'shop'</span><span> </span><span>=></span><span> </span><span>'Shop'</span><span>,</span></p><p><span class="line-number">15</span><span> </span><span>'sponsor'</span><span> </span><span>=></span><span> </span><span>'Sponsor'</span><span>,</span></p><p><span class="line-number">16</span><span> </span><span>'laravel_version'</span><span> </span><span>=></span><span> </span><span>'Laravel v:version'</span><span>,</span></p><p><span class="line-number">17</span><span> </span><span>'php_version'</span><span> </span><span>=></span><span> </span><span>'(PHP v:version)'</span><span>,</span></p><p><span class="line-number">18</span><span>];</span></p></code>
lang/en/welcome.php
For the sake of being specific, we can ignore json
translation files, so create a .gitignore
in the lang
directory.
<code data-theme="github-light" data-lang="text" class="torchlight"><p><span class="line-number">1</span><span># ignore json files</span></p><p><span class="line-number">2</span><span>*.json</span></p></code>
lang/.gitignore
As you can notice, the welcome.php
file now contains keyed texts from the welcome.blade.php
template. Now, replace the strings in the template for the keys we're just defined. Let me mention a couple of examples:
-
<title>Laravel</title>
-><title>{{ __('welcome.laravel') }}
, -
<div ...><a ...>Documentation</a></div>
-><div ...><a ...{{ __('welcome.documentation') }}</a></div>
, -
<div ...>Laravel's robust library of...</div>
-><div ...>{!! __('welcome.vibrant_ecosystem_text') !!}</div>
, -
<div ...>Laravel v{{ Illuminate\Foundation\Application::VERSION }} (PHP v{{ PHP_VERSION }})</div>
-><div ...>{ __('welcome.laravel_version', ['version' => Illuminate\Foundation\Application::VERSION]) }} {{ __('welcome.php_version', ['version' => PHP_VERSION]) }}</div>
.
The first two examples are pretty straightforward; this is standard syntax for displaying the translation string.
In the third example, we have to tell Blade not to escape the data. Although, you should be careful when echoing unescaped content as your app might then be vulnerable to XSS attacks.
Fourth example replaces placeholders we use in the welcome.php
source language file (:version
). To replace the placeholder in the Blade template, pass an array of replacements as the second argument to the __
function.
You can find even more examples including plurals, creating a language switcher (and more detailed explanation) in a great article about How to build a multilingual PHP app with Localazy and Laravel written by Francisco Melo, which was my starting point and an inspiration for composing this post.
Just to test it out, if we refresh our page now - it should look exactly the same as before externalization.
Connect Laravel application to Localazy project
Go to the Localazy signup page and create a free account or log in (if you have already joined our community). Then, name your organization and create a new project.
Select English as the source language. Also, you can enable the Use community translations (ShareTM) option to get some strings translated automatically by Localazy.
On the integrations page, select Laravel integration. Copy the piece of code to the clipboard.
Localazy.com - Laravel integration page
In your project root, create a file called localazy.json
. This file serves as a config file for Localazy CLI. The example above (which we'll modify to our needs) is one of the simplest forms of config. There are many ways how to adjust the localazy.json
file to your needs, everything is described in detail in our documentation.
Paste the code into localazy.json
. This file should be pushed to the Git repository. Although, we should not push the secrets like writeKey
and readKey
. What do we do? Create one more file in the project root named localazy.keys.json
, then cut & paste keys into it. After that, add the latter file to .gitignore
, also located in the project root.
<code data-theme="github-light" data-lang="text" class="torchlight" readability="2"><p><span class="line-number">1</span><span># other .gitignore contents...</span></p><p><span class="line-number">2</span></p><p><span class="line-number">3</span><span># Localazy keys</span></p><p><span class="line-number">4</span><span>localazy.keys.json</span></p></code>
.gitignore
Update localazy.json to fit our application
Next, we have to tweak the localazy.json
file a little bit in order for everything to work correctly. This is the final look:
<code data-theme="github-light" data-lang="json" class="torchlight" readability="4"><p><span class="line-number">1</span><span>{</span></p><p><span class="line-number">2</span><span> </span><span>"upload"</span><span>: {</span></p><p><span class="line-number">3</span><span> </span><span>"type"</span><span>: </span><span>"php"</span><span>,</span></p><p><span class="line-number">4</span><span> </span><span>"files"</span><span>: </span><span>"lang/en/**.php"</span></p><p><span class="line-number">5</span><span> },</span></p><p><span class="line-number">6</span><span> </span><span>"download"</span><span>: {</span></p><p><span class="line-number">7</span><span> </span><span>"files"</span><span>: </span><span>"lang/${lang}/${file}"</span></p><p><span class="line-number">8</span><span> }</span></p><p><span class="line-number">9</span><span>}</span></p></code>
localazy.json
Let me explain: As I've already mentioned, I moved the writeKey
and readKey
to a separate file, so it's not here anymore. Property called upload.files
has changed. The value says that we want to upload all the php
files from lang/en
directory. Files serve as source language files. A detailed explanation can be found in the Upload reference.
There's a similar change in the download.files
section as well. The value of this property instructs CLI to download files with the same name as the uploaded file (placeholder ${file}
). Also, files should be grouped into folders by language (placeholder ${lang}
). All possible options described in detail can be found in the Download reference.
Alright, commit and push our progress; it's time for the next step.
Optional: Test upload locally
Just a reminder - we want to automate our workflow with GitHub Actions. If you don't want to test it locally, skip to the next section. But sometimes, developers would like to test the translations during development (I also wanted to make sure I set up everything properly before getting into GitHub actions). So, if you're interested, I'll show you how.
There are many ways to install the Localazy CLI (depending on several factors). I wanted to use the Docker image to test it out, but unfortunately, Apple Silicon chips are not supported yet, therefore I used an installation via NPM.
Localazy advises to install the package to the system globally, although I installed it in the project folder.
<code data-theme="github-light" data-lang="bash" class="torchlight" readability="2"><p><span class="line-number">1</span><span>npm</span><span> </span><span>install</span><span> </span><span>@localazy/cli</span></p></code>
After that, to test the upload, use:
<code data-theme="github-light" data-lang="bash" class="torchlight"><p><span class="line-number">1</span><span>npx</span><span> </span><span>localazy</span><span> </span><span>upload</span><span> </span><span>-s</span></p></code>
The parameter -s
stands for simulate. It won't actually upload the strings to Localazy, but CLI will certainly tell you if anything possibly went wrong. All good, everything is set up correctly! We can proceed to GitHub Actions.
Add secrets to our repository
To make GitHub Actions work, we need to create secrets in our repository. Why? Later, when we will be using Localazy Upload and Localazy Download Actions, we need them to read writeKey
and readKey
from somewhere (in order to access our Localazy project properly). And as we do not want them to be pushed into the repository (reasons described earlier in the article), we'll add them as secrets.
In repo, navigate to Settings -> Secrets -> Actions
. I named the secrets LOCALAZY_READ_KEY
and LOCALAZY_WRITE_KEY
respectively. Assign both its readKey
/writeKey
value, our configuration should look like this.
laravel-i18n-gh-actions-example repository secrets
Automate Upload with GitHub Actions
In our IDE, create a .github/workflows/upload.yml
file. Alternatively, you could do it all from a repository, go to Actions -> New workflow -> Setup a workflow yourself
. Name it upload.yml
, add the workflow code below and just push it.
This is what the code should look like:
<code data-theme="github-light" data-lang="text" class="torchlight" readability="16"><p><span class="line-number"> 1</span><span>name: Localazy Upload</span></p><p><span class="line-number"> 2</span></p><p><span class="line-number"> 3</span><span>on:</span></p><p><span class="line-number"> 4</span><span> push:</span></p><p><span class="line-number"> 5</span><span> branches: [ develop ]</span></p><p><span class="line-number"> 6</span><span> paths: [ lang/en/**.php ]</span></p><p><span class="line-number"> 7</span></p><p><span class="line-number"> 8</span><span> pull_request:</span></p><p><span class="line-number"> 9</span><span> branches: [ develop ]</span></p><p><span class="line-number">10</span><span> paths: [ lang/en/**.php ]</span></p><p><span class="line-number">11</span></p><p><span class="line-number">12</span><span> workflow_dispatch:</span></p><p><span class="line-number">13</span></p><p><span class="line-number">14</span><span>jobs:</span></p><p><span class="line-number">15</span><span> localazy-upload:</span></p><p><span class="line-number">16</span><span> name: Upload source language strings to Localazy</span></p><p><span class="line-number">17</span><span> runs-on: ubuntu-latest</span></p><p><span class="line-number">18</span></p><p><span class="line-number">19</span><span> steps:</span></p><p><span class="line-number">20</span><span> - uses: actions/checkout@v3</span></p><p><span class="line-number">21</span><span> - uses: localazy/upload@v1</span></p><p><span class="line-number">22</span><span> with:</span></p><p><span class="line-number">23</span><span> read_key: ${{ secrets.LOCALAZY_READ_KEY }}</span></p><p><span class="line-number">24</span><span> write_key: ${{ secrets.LOCALAZY_WRITE_KEY }}</span></p></code>
.github/workflows/upload.yml
Let's dig into the file and explain it. Just to remind you, the general purpose of this action is to upload new source language strings to Localazy on push
or pull_request
in(to) develop
branch.
Facts:
- We assigned a human-readable name Localazy Upload to the action,
- the
on
controls when the workflow will run, - we want to trigger the workflow on
push
orpull_request
intodevelop
branch (given bybranches: [ develop ]
), - at the same time, we want to trigger the workflow only if any of the source language files changed (given by
paths: [ lang/en/**.php ]
), - we'd also like to allow running the workflow manually from the Actions tab (
workflow_dispatch:
), - we defined one
job
(a workflow run is made up of one or more jobs that can run sequentially or in parallel), - this
job
is calledlocalazy-upload
and has it's human-readable name Upload source language strings to Localazy (which then is displayed in GitHub Actions Workflow), - we specified that the type of runner that the job will run on is
ubuntu-latest
, -
job
consists of two steps - a sequence of tasks that will be executed as part of the job, - first step
actions/checkout@v3
checks out your repository under$GITHUB_WORKSPACE
, so your job can access it (documentation here) - second step
localazy/upload@v1
reads the config fromlocalazy.json
and processes uploadwith
usingread_key
andwrite_key
, -
read_key
andwrite_key
values are read fromsecrets.LOCALAZY_READ_KEY
andsecrets.LOCALAZY_WRITE_KEY
variables respectively.
Let's test the workflow now. In our foo
branch, commit and push all the changes we've made. Go to GitHub and make a pull request to develop
.
Create pull request develop <- foo
As soon as the pull request is created, our workflow is triggered. You can tell by Some checks haven't completed yet. Also, there's an orange circle next to Localazy Upload workflow, which means it's running.
Pull request with running workflow
Clicking on Details, we can display the details of the steps of the workflow which is currently running.
Upload workflow result
Everything processed correctly! You can also click on an arrow next to each step to see its details. For example, if we click on Run localazy/upload@v1 action output.
<code data-theme="github-light" data-lang="text" class="torchlight" readability="36"><p><span class="line-number"> 1</span><span>Localazy CLI, v1.6.0</span></p><p><span class="line-number"> 2</span><span>Command-line tool for the Localazy platform.</span></p><p><span class="line-number"> 3</span></p><p><span class="line-number"> 4</span><span>Read more information at https://localazy.com/docs/cli</span></p><p><span class="line-number"> 5</span></p><p><span class="line-number"> 6</span><span>Parameters:</span></p><p><span class="line-number"> 7</span><span> - deprecate missing: no</span></p><p><span class="line-number"> 8</span><span> - import as new: false</span></p><p><span class="line-number"> 9</span><span> - force current: false</span></p><p><span class="line-number">10</span><span> - filter source: true</span></p><p><span class="line-number">11</span><span> - app version: 0</span></p><p><span class="line-number">12</span><span> - groups: (default only)</span></p><p><span class="line-number">13</span><span> - folder: .</span></p><p><span class="line-number">14</span></p><p><span class="line-number">15</span><span>Processing files...</span></p><p><span class="line-number">16</span></p><p><span class="line-number">17</span><span>lang/en/welcome.php</span></p><p><span class="line-number">18</span><span>(file: welcome.php, lang: inherited, type: php)</span></p><p><span class="line-number">19</span></p><p><span class="line-number">20</span><span>lang/en/validation.php</span></p><p><span class="line-number">21</span><span>(file: validation.php, lang: inherited, type: php)</span></p><p><span class="line-number">22</span></p><p><span class="line-number">23</span><span>lang/en/auth.php</span></p><p><span class="line-number">24</span><span>(file: auth.php, lang: inherited, type: php)</span></p><p><span class="line-number">25</span></p><p><span class="line-number">26</span><span>lang/en/passwords.php</span></p><p><span class="line-number">27</span><span>(file: passwords.php, lang: inherited, type: php)</span></p><p><span class="line-number">28</span></p><p><span class="line-number">29</span><span>lang/en/pagination.php</span></p><p><span class="line-number">30</span><span>(file: pagination.php, lang: inherited, type: php)</span></p><p><span class="line-number">31</span></p><p><span class="line-number">32</span><span>Verifying...</span></p><p><span class="line-number">33</span></p><p><span class="line-number">34</span><span>Validating...</span></p><p><span class="line-number">35</span></p><p><span class="line-number">36</span><span>Uploading 3 kB...</span></p><p><span class="line-number">37</span></p><p><span class="line-number">38</span><span>Upload results: 126 added, 0 updated, 0 deprecated</span></p><p><span class="line-number">39</span><span>Using 397 out of 45000 source keys</span></p><p><span class="line-number">40</span></p><p><span class="line-number">41</span><span>Your app on Localazy: https://localazy.com/p/laravel-i18n-gh-action-example</span></p><p><span class="line-number">42</span></p><p><span class="line-number">43</span><span>Done.</span></p></code>
localazy/upload@v1 action output
Great, let's go to the application in Localazy and check the File Management section. As we could see, all files are available there.
Localazy - File management
Translate your texts in Localazy
Now, add a couple of languages and translate and approve some phrases.
Localazy offers three approaches to choose from and combine to translate your project:
Latest Breaking News