A few years ago, when working on global products, the thing I dreaded most was hearing a PM say: "We need to support multiple languages for this release—eight locales in total."
Anyone who has been through this knows that traditional internationalization (i18n) workflows are plagued by tedious, mechanical grunt work: developers write t('home.header.title') in the code, export the Chinese copy into an Excel sheet, and hand it off to the operations or translation team; two weeks later, the translations come back, and you painstakingly copy and paste them back into JSON files for each language. If you miss a single key, or if the PM changes a line of copy at the last minute, the page goes live nakedly displaying home.header.title—instantly sending your blood pressure through the roof.
After doing frontend for long enough, you develop an obsession with "being lazy." Since this entire workflow essentially boils down to text extraction, diffing, format conversion, and type constraints, it can be completely automated with tooling. Today, let's talk about how I built a lightweight CLI tool in Go to automate this whole pipeline.
Pain Point Retrospective: How Much Time We Waste on i18n
Breaking down a few core issues encountered in daily development:
- Inefficient extraction: After finishing a new module, you are left with a screen full of hardcoded text; manually extracting them into i18n keys is time-consuming and error-prone.
- Chaotic key management: Multi-language JSON files easily run thousands of lines long. Over iterations, tons of unused "ghost keys" accumulate, or certain languages end up missing keys entirely.
- Lack of type safety: In a TypeScript project, misspelling a key triggers no static check warnings, and the bug only surfaces at runtime—or worse, in production.
While the Node.js community has tools like i18next-parser, issues like heavy bundle sizes and dependencies on node_modules still feel like a nuisance when it comes to cross-project distribution, CI pipeline execution speed, and integration with internal company platforms.
Why Write This Tool in Go
When building component libraries or frontend build pipelines, Node/TS is my go-to. But for standalone utility CLIs, I now almost always use Go. The reasons are straightforward:
- Single-binary distribution: Compiling down to a single binary means you can drop it straight into
/usr/local/binor a CI image, without requiring ops to configure a Node runtime and install dependencies. - Concurrency and performance: Scanning tens of thousands of source files, performing text matching, and running recursive diffs across multi-language JSON files—Go's concurrency and startup speed make this take mere milliseconds.
- Cross-platform compatibility: The team uses a mix of Mac, Linux, and Windows; a single
cross-compilecovers everyone.
Core Design & Hands-on Implementation
This CLI tool mainly serves three responsibilities:
- Recursively scan the
localesdirectory to find missing keys across different languages. - Automatically generate TypeScript type declaration files based on the primary language JSON (usually
zh-CN.json). - Check for unreferenced "dead keys" in the codebase.
Let's focus on the logic behind flattened key comparison and TS declaration generation.
1. Flattening and Diff Logic
Nested JSON can be tricky to compare, so we first flatten nested map[string]interface{} into dot-separated paths like home.user.title:
package main
import (
"encoding/json"
"fmt"
"os"
)
// FlattenMap 递归将嵌套 JSON 展平为点分路径
func FlattenMap(prefix string, nested map[string]interface{}, flat map[string]string) {
for k, v := range nested {
fullKey := k
if prefix != "" {
fullKey = prefix + "." + k
}
switch child := v.(type) {
case map[string]interface{}:
FlattenMap(fullKey, child, flat)
case string:
flat[fullKey] = child
default:
flat[fullKey] = fmt.Sprintf("%v", child)
}
}
}
// FindMissingKeys 找出 target 相比 base 缺失的 key
func FindMissingKeys(base, target map[string]string) []string {
var missing []string
for k := range base {
if _, exists := target[k]; !exists {
missing = append(missing, k)
}
}
return missing
}
2. Generating TypeScript Type Guards
The best part of writing frontend code is autocomplete. Based on the flattened key collection, we can automatically generate a i18n.d.ts:
package main
import (
"fmt"
"os"
"sort"
"strings"
)
func GenerateTypeDef(flatKeys map[string]string, outputPath string) error {
var keys []string
for k := range flatKeys {
keys = append(keys, fmt.Sprintf(" | '%s'", k))
}
sort.Strings(keys)
content := fmt.Sprintf(`// Auto-generated by i18n-cli. DO NOT EDIT.
export type I18nKey =
%s;
declare module '@/utils/i18n' {
export function t(key: I18nKey, params?: Record<string, any>): string;
}
`, strings.Join(keys, "\n"))
return os.WriteFile(outputPath, []byte(content), 0644)
}
In the frontend project, the wrapped t() function imports this type declaration directly. As soon as you type t(', VS Code accurately autocompletes all available keys. Typos immediately trigger a red underline, nipping issues in the bud during the compilation phase.
Integrating into the Frontend Engineering Workflow
With the tool built, the key is how to integrate it into daily workflows. Our current approach combines two lightweight layers:
-
Local Development (Git Hooks): Add a check to
lint-stagedor pre-commit hooks. When a developer commits code with changes tolocales/zh-CN.json, it automatically triggers the Go CLI to regeneratei18n.d.tsand stages the updated definitions with the commit. -
Pipeline Guardrails (CI Gate): Add an i18n validation stage to GitLab CI / GitHub Actions:
yamli18n-check: stage: test script: - i18n-cli check --base=src/locales/zh-CN.json --target=src/locales/en-US.jsonIf missing keys are found, the pipeline fails immediately and highlights the specific missing fields, preventing broken code from going live.
Summary
Engineering efficiency doesn't always require multi-month overhauls. Often, simply observing repetitive, draining, and error-prone friction points in the team, and taking half a day to write a small utility of a few dozen or hundred lines to close the loop, delivers remarkably immediate returns.
After spending a long time within the frontend bubble, stepping out occasionally to build tools and underlying infrastructure in a more systems-level language like Go broadens your perspective significantly. Offload the tedious work to machines so engineers can focus their energy on what really matters: business architecture and interactive experience optimization.
License: CC BY-NC 4.0
Updated 2 hours ago
Was this article helpful? Give it a like.
0 comments


