How to Easily Distribute to 97 Countries in 32 Languages
1
We'll be doing 'localization' this time. But without the 'hassle'...
Hello, I'm Leedi, an iOS engineer for Alarmy.
How is your localization productivity doing?
Today, I’m going to talk about DelightRoom's localization process, which distributes to 97 countries in over 30 languages.
First, here is a brief overview of our current localization pipeline.
I'll be explaining this again later anyway, so you don't need to understand it completely; just getting a feel for it is enough.
The DesignGroup first uploads the necessary localization strings to Lokalise.
Lokalise creates a PR in the alarmy-ios-localise repo (submodule).
We generate a Localizations.swift file holding the keys as static properties via the LaurineGenerator script.
A python script performs a validCheck, and if it passes, it automatically merges.
(Success/failure is sent as a slack message)The submodule dispatches a submodule update request to the alarmy-ios repo.
The develop branch of the alarmy-ios repo updates the submodule to the latest version.
Among these, the green ones [1,2] are the design team's work, and the blue ones [3,4,5,6] are automated actions using github action.
In other words, there is currently zero effort made for localization in DelightRoom's iOS group.
Especially since step 4 was recently implemented, we even saved the cost of reviewing PRs coming up in the group. And today, I want to talk about this story.
Here is an explanation of each strategy.
(The numbers below are the order of work and are unrelated to the pipeline order above.)
1 . Localizations.swift
Back in April 2022, when I first joined DelightRoom, the DelightRoom iOS group was also doing localization work in a highly inefficient way.
(Having to copy and paste everything by hand...)
The only saving grace was that we had some level of automation where building in Xcode after copy-pasting would run a script called LaurineGenerator to create a Localizations.swift file containing key values as static properties. And we were using it with a slight modification to fit the team's needs.
LaurineGenerator created that file based on the keys in Base.lproj, and unsurprisingly, we were still making quite a few mistakes.
ex) Entering a key value different from Base, or omitting it entirely..
Because of the inefficiencies arising here, in May, a month after I joined, iOS also adopted Lokalise, a localization support tool that was already being used by the Android group.
2. Lokalise
So, what is Lokalise?
Lokalise is a cloud-based platform that helps efficiently manage the localization process for various digital products like software, websites, mobile apps, and games. Localization involves tasks related to multilingual support, playing a crucial role in translating and localizing content so that users can use the product globally.
That was a very neat summary provided by GPT.
In short, it's a tool that makes supporting multiple languages for a product effortless.
To give you a quick example,
You fill in the Key and the values for each language like this. (Empty fields are automatically filled with the Base language.)
And when you click the Build button like this?!
A PR with the applied changes is generated like this.
How about that?
Super easy, right?
Of course, there were many incidents during the Lokalise integration process, too,
but that's not really important, so I'll wrap up the explanation here.
3. Submodule
However, problems arose here too.
Looking back, I had clearly defined the problem situation at the time as follows.
That is, it was great that Lokalise automatically created PRs, but having files generated externally where we couldn't manage them led to constant merge conflicts, and it was also causing inefficiencies in our github strategy.
So, I proposed using a submodule approach as a strategy to solve this.
What is a submodule?
A Git Submodule is a feature that allows you to include another Git repository within a Git repository. This lets you include and manage independent external projects or libraries within your main project. -by GPT
Simply put, a submodule is an independent repository, and the main repository can pin and use a specific version via a commit hash.
In our case, if we made Localization a submodule, the develop branch of the main repository would always point to the latest commit address, and each branch (hotfix or feature branch) could flexibly change it as needed.
Now, we were finally able to easily hop between localization versions however we wanted. By moving the address wherever we needed at any time, we could easily apply the desired version of localization without needing to Rebase or cherrypick anymore.
But it would be a shame to stop here, right?
4. github action
As I mentioned in step 1, we were managing files through LaurineGenerator. And since localization management was moved out into a submodule, managing it from Xcode felt weird.
A teammate updates the submodule, builds Xcode, and merges it back?
Just imagining it is inefficient.
Therefore, we decided to get friendly with github action, a good buddy that could solve this.
What is github action?
GitHub Actions is a CI/CD(Continuous Integration/Continuous Deployment) tool provided by GitHub. With it, you can automatically build, test, and deploy code, or automate various workflows.
It's easiest to understand it as a buddy inside github that performs an action based on a specific trigger.
The first goal was very simple.
When a PR starting with lokalise is opened, execute localization.sh. Then commit the changes.
on:
pull_request_target:
types:
- opened
jobs:
localization:
if: startsWith(github.head_ref, 'lokalise')
runs-on: macos-latest
steps:
- name: Check out lokalise branch
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
- name: Pull Request
run: |
git pull origin
- name: Run Script
id: script
run: |
ls -al
chmod +x ./Scripts/localization.sh
./Scripts/localization.sh
- name: commit and push
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "[Success] complete localization build"It looked clean.
Now, after a teammate merges into the submodule, the develop branch in the main repo just needs to update the submodule to the latest.
But it's not actually clean, is it?
That's right.
A teammate has to merge AND update develop to the latest.. that's too much of a hassle, isn't it?
5. dispatch submodule
So, one more goal was added.
When a PR is merged, I want the main repository to automatically update to the latest!
I didn't know much about github action either, so I wasn't sure if this was technically possible (though I figured it definitely would be), but when I looked it up, it was already easily doable.
permissions: write-all
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Trigger Workflow
uses: actions/github-script@v6
with:
github-token: ${{ secrets.BUILDER_BOT_1 }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: 'delightroom',
repo: 'alarmy-ios',
workflow_id: 'update_submodule.yml',
ref: 'develop',
})If the submodule throws it like this
on:
workflow_dispatch:
permissions: write-all
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
token: ${{ secrets.BUILDER_BOT_1 }}
- name: Pull & update submodules recursively
run: |
git submodule update --init --recursive
git submodule update --recursive --remote
- name: Commit & push changes
run: |
git config --global user.name 'GitHub Actions'
git config --global user.email 'builder-bot-1@delightroom.com'
git add .
git commit -am "Update submodules"
git pushThe main-repo catches it perfectly like this and updates the submodule to the latest.
So now, even the annoying submodule update is handled!
We have defeated the enemies of localization!
6. Review.. Review..
We weren't the only ones whose productivity improved, so the speed of applying localization got faster and faster, and the number of PRs Lokalise generated gradually increased.
Just like how Coupang Rocket Delivery order volumes exploded once free shipping was introduced.
Initially, it wasn't a problem.
We just had to check the very last one and merge it anyway.
Closing the previous PRs while we were at it.
However, the PRs piling up every time we visited the submodule repo messed with our minds, and since there were only a few things the engineering team actually checked (honestly, as long as the Keys are fine, it's all good..), we decided to automate this part too.
Moreover, unlike when we did the initial setup, we had the reliable Claude and GPT by our side, so there was absolutely no difficulty in making this decision.
Putting all our trust in our AI friends, the automation train set off and yielded this cute little script that runs a python script (validate_variables.py) once localization is done.
review-and-merge:
needs: localization
if: ${{ needs.localization.result == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
- name: Checkout base branch
uses: actions/checkout@v3
with:
path: base_branch
ref: ${{ github.base_ref }}
- name: Get First Commit Message
id: commit
uses: actions/github-script@v6
with:
script: |
const { data: commits } = await github.rest.pulls.listCommits({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number
});
const firstCommit = commits[0];
core.setOutput('message', firstCommit.commit.message);
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install GitPython
run: pip install gitpython
- name: Run validation script
id: validate
run: |
python Scripts/validate_variables.py
env:
GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Notify Slack on Validation Failure
if: steps.validate.outcome == 'failure'
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
payload: |
message: "❌ PR Validation Failed\n\nPR: ${{ github.event.pull_request.html_url }}\nBranch: `${{ github.head_ref }}`\nCommit: ${{ steps.commit.outputs.message }}\nError: ${{ env.VALIDATION_ERROR }}"
- name: Close PR on Validation Failure
if: steps.validate.outcome == 'failure'
uses: actions/github-script@v6
with:
script: |
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
state: 'closed'
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: `❌ PR Validation Failed\nError: ${process.env.VALIDATION_ERROR}`
});
- name: Merge PR
if: steps.validate.outputs.can_merge == 'true'
id: merge
uses: actions/github-script@v6
with:
script: |
await github.rest.pulls.merge({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ github.event.pull_request.number }},
merge_method: 'squash'
});
continue-on-error: true
- name: Notify Slack on Merge Success
if: steps.merge.outcome == 'success'
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: webhook-trigger
payload: |
message: "🎉 PR Merge Success\n\nPR: ${{ github.event.pull_request.html_url }}\nBranch: `${{ github.head_ref }}`\nCommit: ${{ steps.commit.outputs.message }}
I cobbled this together with my GPT friend and honestly didn't struggle much.. so I don't really have any anecdotes for this part.
And while I was at it, I wanted to receive the job results in slack, so I ended up adding this buddy as well.
(Checking it manually is annoying.. It's not just me, right..?)
And the result?!
You can see it working beautifully like this :)
Now, there is absolutely nothing left for engineers to do for localization.
If the designer puts up a localization PR,
- it will automatically review it, notify us if there are errors, and if everything is normal, it will automatically merge it and finish updating the submodule to the latest version.
Just in case you've been having difficulties with localization work, I've compiled this post about our work history and details in the hopes that you might get some good ideas. I hope this was helpful, and if you can't be bothered to apply it yourself, you are more than welcome to join DelightRoom haha
Thank you for reading to the end.