-An experience separating multilingual (i18n) resources into a separate repository and adding automated validation and deployment-
1. Introduction
The tourism content service I was responsible for supports four languages and five language packs in total: Korean as the base language, English, Uzbek (with both Latin and Cyrillic scripts), and Karakalpak.
All text on the screens, from button labels to informational messages and validation messages, depends on language-specific JSON translation files.
In this article, I summarize my experience separating these translation files, which were managed within the frontend codebase, into a separate repository and creating validation and deployment scripts that automatically detect missing translations.
2. Limitations of the Existing Approach
Before the separation, we kept language-specific JSON files in the frontend project. Whenever a text change was requested, we edited the base-language file and then manually added the same key to each of the other four language files.
This approach had three major problems.
-
Even a minor task such as changing a single piece of text required rebuilding and deploying the entire frontend before the change took effect.
-
Even to fix a single typo, we had to wait for the CI build to finish.
-
Because keys were copied manually for each language, keys were frequently omitted from only certain languages. As a result, the translation key itself was exposed or an empty string was displayed only on screens used by speakers of those languages.
-
There was no validation process to detect these omissions, so there was no way to know about a problem until QA or an actual user found it after deployment.
-
Because Uzbek was managed in separate files for the Latin and Cyrillic scripts, mismatches between the two scripts were particularly common.
3. Improvement: Separating the Language Pack Repository
Based on the idea that translation resources are static files and do not necessarily need to share an application codebase or deployment cycle, we created a repository dedicated exclusively to managing the translation JSON files.
In the actual service infrastructure, there is a Kubernetes nginx Pod that serves static files. Updating only the JSON files in this Pod means that new translations are reflected starting with the next request, without redeploying the frontend.
Separating the resources into their own repository also made it much easier to add validation logic.
The supported languages and structure are as follows.
|
Language Code |
Language |
Notes |
|---|---|---|
|
ko-KR |
Korean |
Base language |
|
en-US |
English |
- |
|
uz-Latn / uz-Cyrl |
Uzbek (Latin/Cyrillic scripts) |
Managed separately by script |
|
kaa-Latn |
Karakalpak (Latin script) |
- |
Each language folder is divided into four files by screen area: common, admin, validation, and user.
This reduces conflicts caused by multiple people editing one large file simultaneously and makes it immediately clear which area is affected when a problem occurs.
4. Automatic Translation Key Validation: validate.js
We wrote the validation script using only the Node.js built-in modules (fs, path).
The key idea is to use Korean as the reference and compare the key sets in the remaining language files. Because the JSON has nested structures such as button.save, we first flatten the structures into dot-notation paths and then compare them.
function collectKeys(obj, prefix = "") {
const keys = [];
Object.keys(obj).forEach((key) => {
const currentPath = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === "object" && obj[key] !== null) {
keys.push(...collectKeys(obj[key], currentPath));
} else {
keys.push(currentPath);
}
});
return keys;
}
Keys that exist in the base language but are missing from the target language are classified as Error, while keys that exist only in the target language are classified as Warning.
If there is even one Error, which represents a genuine problem that can break the screen, the script returns an exit code through process.exit(1). Because this script is called as the first step of the deployment script, an abnormal termination here prevents the subsequent deployment steps from running at all.
This structure makes deployment physically impossible when translations are missing.
5. Automated Deployment Pipeline
Deployment consists of five steps: validation → tar compression → automatic nginx Pod discovery → copying to the Pod with kubectl cp → extraction and cleanup inside the Pod.
Because the team includes both macOS and Windows users, we created separate bash and PowerShell scripts and made sure that both always run in the same order.
Because the Pod name changes with every redeployment, we did not hardcode it; instead, we query it each time.
$POD = kubectl get pod -n $NAMESPACE --no-headers |
Where-Object { $_ -match "^nginx" } |
ForEach-Object { ($_ -split "\s+")[0] } |
Select-Object -First 1
For reference, running tar on macOS also creates metadata files in the form ._*. If these files remain inside the Pod, they make the static directory cluttered.
The bash script removes them using the COPYFILE_DISABLE=1 environment variable and the --exclude option. Windows tar does not create these metadata files in the first place, so the PowerShell script has no corresponding logic.
Rather than forcing the two scripts into one, we chose to retain only the processing actually needed for each platform after running and verifying them directly in each environment.
Troubleshooting: Deployment That Failed Only on Windows
Initially, we grouped several shell commands to run inside the Pod—changing directories, extracting archives, deleting files, and listing contents—into a single here-string and passed it as a whole to kubectl exec.
# 최초 버전 — 여러 명령을 하나의 here-string으로 묶어 전달
$cleanupScript = @"
cd $NGINX_HTML
tar -xvf $TAR_NAME
rm $TAR_NAME
"@
kubectl exec -n $NAMESPACE $POD -- /bin/sh -c $cleanupScript
This approach worked without issues in macOS bash, but Windows PowerShell handles passing arguments to external processes (kubectl.exe) differently, so strings containing line breaks were truncated in the middle or split line by line. As a result, an error occurred in which only half of the command was executed inside the Pod.
After identifying the cause, we abandoned the approach of combining multiple commands into one and split the kubectl exec calls by command.
# 수정 버전 — 명령을 kubectl exec 호출 단위로 분리
kubectl exec -n $NAMESPACE $POD -- rm "${NGINX_HTML}/locales"
kubectl exec -n $NAMESPACE $POD -- tar -xvf "${NGINX_HTML}/${TAR_NAME}" -C $NGINX_HTML
kubectl exec -n $NAMESPACE $POD -- rm "${NGINX_HTML}/${TAR_NAME}"
Splitting the commands into individual calls eliminated the issue of PowerShell truncating the arguments.
The number of calls increased to four, but since these were local operations on a single Pod, the cost was negligible. This experience also reminded me that cross-platform scripts are not truly complete until they have been run directly in both environments.
6. Benefits and Conclusion
Since separating the repository and adding automation, text updates have been completely decoupled from frontend deployments and can be applied within seconds with a single script run. Missing translations are also detected mechanically before deployment, eliminating cases where only users of a particular language saw a broken screen.
In addition, deployments can now be performed using the same procedure on both macOS and Windows, reducing the burden of having deployment knowledge concentrated in a single person.
This was not a project that introduced any flashy new technology.
It consisted entirely of combining familiar tools such as a roughly 100-line validation script, kubectl, and tar. Nevertheless, it was meaningful because it precisely identified and resolved problems that had actually been recurring, such as the burden of redeployment, missing translations, and platform-specific script errors.
In particular, by resolving issues one by one that only become apparent through direct execution, such as the kubectl exec argument-passing problem reproducible only on Windows, I would like to improve the process next by connecting the validation step to GitLab CI so that problems are automatically filtered out before merging.
zero