Internationalizing a Legacy Codebase Isn't a Translation Job — It's an Architecture Overhaul
theme: devui-blue
Foreword
Recently, I've been working on the internationalization (i18n) refactoring of an old project.
The project has been running for many years, with many pages, many business modules, and many historical coding styles. We first scanned it with a script and found that there are roughly tens of thousands of Chinese strings that need to be handled.
Seeing this number, the first reaction is often: just assign a few more people, replace all the Chinese with t(), and add a few language packs, right?
After actually starting, I realized things are far from that simple.
Some Chinese strings are just text on buttons, which can be translated directly without issue; but other Chinese strings are used for making judgments, passing to APIs, or storing in caches. Some are hidden in dynamic menus, shared components, asynchronous tasks, exported file names, and print templates.
If you don't first categorize these situations clearly and just do a global replacement, the pages might appear in English, but the business logic will quietly break.
This article won't talk about a specific project, nor will it show real business code. I mainly want to discuss: facing an old project with tens of thousands of Chinese strings that is still under continuous iteration, how we transformed internationalization from a "translation task" into an "architectural refactoring."
The code in the article is all rewritten as generic examples. Although the examples use common frontend patterns, the overall approach does not depend on Vue, React, or any specific framework.
1. Overall Design Approach
1. The hardest part isn't translation, but separating text from business logic
Let's look at a very common piece of code in an old project:
const orderStatusList = [
{ id: 1, name: '待审核' },
{ id: 2, name: '已通过' }
]
if (currentStatusName === '待审核') {
openReviewDialog()
}
On the surface, 待审核 is a piece of display text.
In reality, it also participates in business logic.
If you only translate the name in the list to Pending Review, the judgment will fail, and the console won't throw any errors.
This type of problem is much more serious than missing a button translation. A missed translation is visible at a glance, but a logic failure might only be exposed when an online user performs an operation.
So we first established the most important rule:
Translation is only responsible for display; it must not participate in parameter passing, caching, or business logic.
Judgments should use stable id, code, or key:
const orderStatusList = [
{
code: 'pending_review',
name: '待审核',
nameI18nKey: 'orders.status.pendingReview'
},
{
code: 'approved',
name: '已通过',
nameI18nKey: 'orders.status.approved'
}
]
if (currentStatus === 'pending_review') {
openReviewDialog()
}
Three fields each do their own job:
codeis used for business logic;nameI18nKeyis used for UI translation;nameis temporarily kept as a fallback for old pages and exceptional situations.
Internationalization can change what the user sees, but it cannot change the data the system processes.
2. 50,000 Chinese strings do not directly equal 50,000 translation tasks
Scanning tools can only tell us where Chinese strings are, not what the Chinese strings are used for.
For example:
const title = '订单管理'
const colorName = '中国红'
const fileName = '订单列表'
const userRemark = '用户自己填写的中文'
Their handling methods are completely different:
titleis UI copy and needs translation;colorNamemight be a business value and cannot be changed arbitrarily;fileNamerequires first confirming the export language rules;- User-generated content usually does not need translation.
So the first step is not to arrange for everyone to do batch replacements, but to scan, categorize, and define boundaries.
We roughly divided the Chinese strings into several categories:
Regular page copy
Shared enums and configurations
Dynamic menus and backend messages
Display text in Store and cache
Special outputs like exports, prints, images
User-generated content
Only the first category is most suitable for direct conversion to t().
3. Don't aim to finish everything at once
Facing 50,000 Chinese strings, the most dangerous plan is "finish all changes in this version."
The project is still iterating normally, and new requirements may continue to add Chinese strings every day. Shared components, routes, and request layers are used by many pages. The longer a large branch drags on, the higher the risk of conflicts and regressions.
We broke the refactoring into four phases.
Phase 1: Build the foundation first, then run a complete pilot
First handle:
- Language initialization;
- Language pack loading;
- Route titles;
- Component library language;
- Shared selectors and filter components;
- Request errors;
- Language switching and cache clearing;
- Fallback for missing translations.
Then choose a business module that includes lists, details, filters, dialogs, and message notifications to run through completely.
Don't just test with the login page.
The login page is certainly simple, but it cannot validate dynamic menus, shared enums, table configurations, and complex states.
Phase 2: Migrate module by module
Language packs are first released together with the frontend code.
Merge a module as soon as it's done, without waiting for other modules. This allows normal releases and avoids creating a huge branch that can't be merged for months.
Phase 3: Move language packs to CDN
After the local language pack directory structure, keys, and loading methods are stable, switch to CDN.
Business pages should not need to be re-modified just because the storage location of language packs changed.
Phase 4: Build the language management backend last
After the rules are stable, support online editing, version releases, canary releases, and rollbacks.
If you build the management backend at the very beginning, the frontend namespace and key rules are still changing, and the backend will likely need rework.
4. Pages don't care where the language pack is stored
Business code only depends on two capabilities:
loadNamespaces(locale, namespaces)
t(key, defaultMessage)
Whether the language pack is currently stored locally or pulled from a CDN later, the page doesn't need to know.
The overall relationship is roughly:
Pages and shared components
↓
Unified i18n runtime
↓
Language pack loader
├─ Local JSON
├─ CDN
├─ IndexedDB cache
└─ Local fallback pack
The benefit of this is straightforward: in the first two phases, stabilize the business migration first; in the third phase, only swap the loader, without touching business pages extensively again.
2. What are the special difficulties of internationalizing old projects?
The following issues are what truly consume time in old project refactoring.
Each issue is described in the same order: how it was originally written, why it breaks, and how it was finally fixed.
1. Shared enums cannot be directly changed to t()
If a set of options is only used on the current page, not exported, and not stored in cache, direct translation is fine:
const weightOptions = [
{
value: 'actual',
name: t('settings.weight.actual', '使用实际重量')
},
{
value: 'estimated',
name: t('settings.weight.estimated', '使用预估重量')
}
]
But shared enums cannot copy this pattern:
// Not recommended to write directly in a shared module like this
export const orderTypeList = [
{ code: 'normal', name: t('orders.type.normal') }
]
In old projects, a shared name is often not just responsible for display, but may also be used for:
- API parameters;
- Default configurations;
- Cache merging;
- Map keys;
- String concatenation;
- Conditional judgments.
After changing name to English, these places will change together.
Shared enums are more suitable for retaining the original value and adding a key:
export const orderTypeList = [
{
code: 'normal',
name: '普通订单',
nameI18nKey: 'orders.type.normal'
}
]
Generate a new array for display:
const displayOrderTypes = orderTypeList.map(item => ({
...item,
name: resolveI18nText(item, 'name')
}))
The original array remains unchanged, and business logic continues to use code.
2. Chinese display text might be controlling business behavior
This kind of code is very subtle:
const noClickColumns = ['操作']
if (!noClickColumns.includes(column.label)) {
selectRow(row)
}
After the column header is translated to Actions, the "操作" column will also trigger row selection.
The correct approach is to look at stable fields:
const noClickColumnKeys = ['actions']
if (!noClickColumnKeys.includes(column.property)) {
selectRow(row)
}
Before refactoring, it's best to do a targeted scan:
column.label === 'Chinese'
dialog.title === 'Chinese'
route.meta.title === 'Chinese'
name === 'Chinese'
translatedText.includes(...)
Anywhere translated text participates in ===, includes, switch, it must be carefully reviewed.
3. Changing a shared component can affect hundreds of pages
In large projects, many pages use the same selectors, filter bars, tables, and date components.
If every page manually writes:
options.map(item => ({
...item,
name: t(item.nameI18nKey)
}))
After a few months, many different versions will inevitably appear.
A more efficient approach is to make shared components aware of nameI18nKey:
const getOptionLabel = (item: Record<string, string>) => {
return resolveI18nText(item, props.labelField)
}
Business pages continue to pass the original data:
<SmartSelect
v-model="status"
:options="orderStatusList"
label-field="name"
value-field="code"
/>
Fixing one shared component can cover many pages.
But shared components also have the largest impact, so you must first clean up the "display text participating in judgments" issue mentioned in the previous section, then gradually roll out and verify.
4. Store and cache store already-translated text
This kind of code is very common:
taskStore.addTask({
title: '批量导出',
columns: [
{ prop: 'operation', label: '执行操作' },
{ prop: 'reason', label: '失败原因' }
]
})
After switching languages, the task is still there, and the Chinese text inside is also still there.
It's more reasonable to save the key and parameters:
taskStore.addTask({
titleI18nKey: 'tasks.batchExport',
title: '批量导出',
columns: [
{
prop: 'operation',
labelI18nKey: 'tasks.operation',
label: '执行操作'
},
{
prop: 'reason',
labelI18nKey: 'tasks.failureReason',
label: '失败原因'
}
]
})
Translate when the task component renders.
The same principle applies to:
- Breadcrumbs;
- Tabs;
- Table column settings;
- Notification center;
- Data restored across pages.
The state layer saves "what it is," and the display layer decides "how to display it now."
5. Dynamic menus cannot rely on Chinese names for reverse lookup
Static routes are relatively simple:
meta: {
title: '订单管理',
titleI18nKey: 'route.orders'
}
Dynamic menus are more troublesome because the names usually come from the backend.
In the long term, the backend should ideally return a stable key:
{
"path": "/orders",
"name": "订单管理",
"nameI18nKey": "menu.orders"
}
The frontend prioritizes using nameI18nKey, falling back to name if absent.
If the backend cannot be changed in the short term, you can first use permission codes or menu codes for mapping:
const menuI18nMap = {
order_manage: 'menu.orders',
inventory_read: 'menu.inventory'
}
But do not use Chinese menu names for mapping:
// Not recommended
const menuMap = {
订单管理: 'Order Management'
}
If the menu name changes, the mapping breaks. Menus with the same name also cannot be distinguished.
6. If the backend only returns a Chinese sentence, the frontend truly cannot translate it
If the API only returns:
{
"msg": "当前记录已被其他用户修改"
}
The frontend has no reliable way to know which translation it corresponds to.
A more suitable protocol is:
{
"code": 40901,
"messageKey": "errors.resourceChanged",
"params": {
"resource": "订单"
},
"msg": "当前记录已被其他用户修改"
}
The frontend can process in this order:
Backend messageKey
→ Local key corresponding to code
→ Key corresponding to HTTP status code
→ Backend msg
→ Generic error message
This allows gradual refactoring without breaking old APIs before the backend is fully updated.
If the project has multiple request wrappers, they must share the same error parsing function. Otherwise, it's easy to end up with API A reporting errors in English while API B reports errors in Chinese.
7. Concatenated sentences cannot just translate the words inside
Very common in Chinese code:
const message = `是否对选中的 ${total} 条记录执行${actionName}?`
If you only translate actionName, the whole sentence remains in Chinese word order.
The complete sentence should be placed in the language pack:
t(
'orders.batchActionConfirm',
'是否对选中的 {total} 条记录执行{action}?',
{
total,
action: t(actionI18nKey, actionName)
}
)
English can adjust its own word order:
{
"batchActionConfirm": "Apply {action} to {total} selected records?"
}
Internationalization is not word replacement. Different languages may have different word orders, singular/plural forms, and expression habits.
8. Native confirm and HTML dialogs cannot mechanically apply t()
Plain text confirmation dialogs are easy to handle:
await showConfirm(
t('orders.deleteConfirm', '确认删除该订单吗?'),
t('common.tip', '提示')
)
Complex situations are different:
showMessage({
useHtml: true,
content: '<strong>账号已存在,<a onclick="goLogin()">立即登录</a></strong>'
})
Here, copy, HTML, and click events are bound together in one string.
After translation, the word order might change, tag positions might change, and there are security concerns. A more suitable approach is to make it a normal component, letting text go through the language pack and links use real events.
Native confirm() should also be replaced as much as possible. Its OK and Cancel buttons usually follow the browser or OS language, which may not match the language selected within the application.
9. Export language and UI language are not necessarily the same
Switching the page to English does not mean the exported file must be in English.
Some users operate in an English UI, but the files need to be sent to a Chinese team; some printed labels must adhere to a fixed format and cannot change arbitrarily with the UI.
So first, separate these concepts:
UI language
Export language
Print language
User-generated content
Export tools can support both keys and old file names simultaneously:
exportExcel({
fileNameKey: 'exports.orderList.fileName',
fileName: '订单列表',
sheetNameKey: 'exports.orderList.sheetName',
sheetName: '订单明细',
data
})
Print fields should also not only keep a name:
interface PrintField {
fieldCode: string
name?: string
nameI18nKey?: string
printLabel?: string
}
fieldCodeis used for internal identification;nameI18nKeyis used for the editor interface;printLabeldetermines what is actually printed on paper.
If you directly change all historical name to t(), the UI translation might be fine, but the print template will likely break.
10. Chinese in images cannot be saved by language packs
No matter how you call t(), Chinese in PNGs and JPGs cannot be changed because the text is already in the image pixels.
There are generally three ways to handle this:
- Extract the text from the image and render it with regular DOM;
- Prepare one image for each language;
- Explicitly decide this resource will not be multilingual and record it as an exception.
Extract whenever possible. Maintaining a set of images for each language makes it easy to miss updates over time.
3. How to implement specifically?
1. Split language packs by business module, not by individual page
Splitting language packs too finely results in many small files and requests.
Putting everything in one large file is also problematic:
- Large initial load;
- Easy conflicts when multiple people modify;
- Changing one small piece of copy requires updating the entire pack;
- Hard to know which key belongs to whom.
A more suitable boundary is: one secondary business entry corresponds to one namespace, and its lists, details, edit pages, dialogs, and exports are all placed together.
For example:
locales/
├─ zh-CN/
│ ├─ common.json
│ ├─ route.json
│ ├─ auth.json
│ ├─ admin_orders.json
│ └─ admin_inventory.json
└─ en-US/
├─ common.json
├─ route.json
├─ auth.json
├─ admin_orders.json
└─ admin_inventory.json
common only holds truly universal words, like save, cancel, confirm.
Don't see the word "status" everywhere and stuff it all into common. Order status and inventory status might be expressed differently in different languages; forcing them to share is actually harder to maintain.
2. Load using a unified loader
Routes declare which language packs they need:
{
path: '/orders',
meta: {
titleI18nKey: 'route.orders',
i18nNamespaces: ['common', 'route', 'admin_orders']
}
}
Load uniformly before entering the route:
router.beforeEach(async (to) => {
const namespaces =
to.meta.i18nNamespaces ?? ['common', 'route']
await loadNamespaces(currentLocale(), namespaces)
})
The loader also needs to handle two minor issues:
- Don't load the same language pack repeatedly;
- If multiple requests come at the same time, don't initiate duplicates.
const loaded = new Set<string>()
const inflight = new Map<string, Promise<void>>()
export const loadNamespace = (
locale: string,
namespace: string
) => {
const key = `${locale}:${namespace}`
if (loaded.has(key)) {
return Promise.resolve()
}
if (inflight.has(key)) {
return inflight.get(key)!
}
const task = loadMessage(locale, namespace)
.then(message => {
mergeLocaleMessage(locale, namespace, message)
loaded.add(key)
})
.finally(() => {
inflight.delete(key)
})
inflight.set(key, task)
return task
}
The cache key must include the language.
If you only record the namespace, after loading Chinese first and then switching to English, the loader will mistakenly think this pack has already been loaded.
3. Use an adaptation layer to be compatible with old and new data
Old projects are not suitable for deleting all Chinese fields at once.
Suppose hundreds of pages are reading:
item.name
If you delete name all at once and require all pages to be changed simultaneously, the risk is too high.
During the migration period, allow:
{
code: 'pending',
name: '待处理',
nameI18nKey: 'orders.status.pending'
}
Migrated pages prioritize reading nameI18nKey, while unmigrated pages continue to display the Chinese name.
A unified resolution function can be provided:
export const resolveI18nText = (
item: Record<string, string>,
field = 'name'
) => {
const key = item[`${field}I18nKey`]
const fallback = item[field] ?? ''
return key ? t(key, fallback) : fallback
}
Display fields and keys correspond one-to-one:
name → nameI18nKey
label → labelI18nKey
title → titleI18nKey
message → messageI18nKey
placeholder → placeholderI18nKey
Although this adds a few more fields, the semantics are clearer and less likely to be confused with existing labelKey, nameKey in the old project.
4. When switching languages, old projects can choose to refresh
We initially also wanted to implement a refresh-free switch.
Later, we took stock of the existing state:
- Dynamic route cache has Chinese titles;
- Breadcrumbs store the final text;
- Opened tabs store titles;
- Some Stores have task names and headers;
- Module top-level constants have already been executed;
- The component library also needs to switch languages.
For a very low-frequency language switch operation, making all historical states support real-time response is costly and easy to miss things.
Finally, we chose a more direct approach:
Save target language
→ Clear state containing text
→ Refresh the page
→ Re-initialize with the new language
export const switchLocaleWithReload = (locale: string) => {
localeStorage.set(locale)
systemStore.resetForLocaleChange()
window.location.reload()
}
This doesn't mean deleting all caches.
Token, tenant, user preferences cannot be touched. Only clean up content containing display text, such as dynamic routes, breadcrumbs, tab titles, table column headers, and task center copy.
Refresh is not the flashiest solution, but it's more stable for many old projects. If the product explicitly requires no refresh, then add reactive hot-switching.
5. Move CDN after business migration is stable
At the beginning of internationalization, what changes most are keys and module boundaries.
Today a key is in common, tomorrow you find it should belong to the orders module; today you split packs by page, a few days later you decide to split by business entry.
If you rush to set up CDN, version management, and a publishing backend at this point, it only makes the debugging chain longer.
First, use local JSON to stabilize these things:
- How to divide namespaces;
- How to name keys;
- How routes declare dependencies;
- How shared components resolve;
- How to fallback when keys are missing.
Once these are stable, only swap the loader:
export const loadMessage = async (
locale: string,
namespace: string
) => {
if (!enableCdnI18n) {
return loadLocalMessage(locale, namespace)
}
return loadCdnMessageWithFallback(locale, namespace)
}
Business pages don't need to be re-modified.
6. Multiple fallback layers when CDN fails
CDN might timeout, miss files, or have version mismatches.
The loading order cannot just be "request CDN, show key on failure."
A more reliable order is:
1. Read IndexedDB cache for the current version
2. Request CDN for the current version
3. Use the last successful old version cache
4. Use the frontend's built-in core language pack
5. Fallback to the default language
6. Use the Chinese default text at the `t()` call site
7. Finally, show the key and report it
Provide default text when calling:
t('common.save', '保存')
It's not a substitute for the language pack, but the last line of defense. Even if a route misses loading the pack, the user sees "保存" instead of common.save.
Missing keys should also be reported:
reportMissingI18nKey({
locale,
key,
path: window.location.pathname
})
Fallbacks handle user experience; monitoring ensures problems are eventually fixed.
7. Language packs also need canary releases and rollbacks
After language packs are released independently, they are essentially like regular online configurations.
Before release, at least check:
- Can the JSON be parsed normally;
- Are there missing keys for the target language;
- Are interpolation parameters consistent;
- Have any keys still in use been deleted;
- Do namespaces match file names.
For example, Chinese is:
{
"result": "成功 {success} 条,失败 {failed} 条"
}
If English only has {success} and misses {failed}, it should be blocked at release time.
Each language pack should also have a version number. When a translation issue is found, only rollback the current module, without requiring the entire frontend to be re-released.
4. How to advance in batches with 50,000 Chinese strings?
1. Scan first, then categorize
The scan scope can include:
Chinese in Vue, JSX, and HTML templates
TypeScript, JavaScript strings
Form placeholders and validation messages
Message notifications and confirmation dialogs
Enum name, label, title
Route titles
Export and print configurations
Image file names and static resources
But scan results cannot be directly auto-replaced.
Regular buttons can be auto-processed, but display text participating in judgments, print configurations, and HTML dialogs should be listed first for developers to judge.
2. Separate responsibilities for shared capabilities and business modules
One group is responsible for the shared foundation:
- Loader;
- Routes;
- Request errors;
- Shared components;
- Shared enums;
- Scanning and CI;
- Caching strategy.
Another group takes ownership by business module:
- Lists;
- Details;
- Dialogs;
- Forms;
- Module-private enums;
- Import and export.
Pages under one business entry should ideally be handled by the same person or the same small team. This keeps language pack boundaries clear and reduces conflicts when multiple people modify a large JSON simultaneously.
3. No long-lived large branches
Merge as soon as a module is done.
When shared components change, submit a separate PR; don't casually mix it into thousands of lines of business page changes.
This isn't about pursuing a high commit count, but about ensuring each change can be independently verified and rolled back.
4. What do plugins, scan scripts, and CI each do?
These three are not the same thing.
Editor Plugin: Helps write less wrong code during development
Editor plugins are suitable for:
- Key autocompletion;
- Hover to view translation;
- Jump from code to language pack;
- Warn that the current key might not exist.
It can improve development efficiency, but it's only a personal local tool. If someone doesn't install it or doesn't turn it on, the check doesn't exist, so it cannot be treated as the final quality gate.
ESLint or Framework Plugin: Checks code structure
These plugins are suitable for checking:
- Whether bare text has been added in templates;
- Whether
t()usage conforms to rules; - Whether keys use disallowed formats;
- Whether certain static keys do not exist.
They understand code syntax better and are more accurate than simple regex.
But plugins usually don't know if a name is display text or an API parameter, nor can they see that "a translated title is participating in business logic."
Custom Scan Script: Supplements business rules
Old projects still need their own scripts to check project-specific issues, for example:
Whether display fields are paired with *I18nKey
Whether translation results are written into Store or cache
Whether Chinese participates in ===, includes, switch
Whether language pack keys are missing
Whether interpolation parameters are consistent across languages
Whether the current module still has unhandled Chinese
Commands can be unified:
{
"scripts": {
"i18n:lint": "eslint src --max-warnings=0",
"i18n:scan": "node scripts/scan-i18n.mjs",
"i18n:check": "pnpm i18n:lint && pnpm i18n:scan"
}
}
The names above are just generic examples; the point is to provide a single entry point for both local and CI.
CI: Ensures everyone must check
CI doesn't need to re-implement scan logic; it just executes the unified command:
- run: pnpm install --frozen-lockfile
- run: pnpm i18n:check
This way, regardless of whether the developer has installed an editor plugin, the code will go through the same set of checks after submission.
Simply put:
Editor plugin is responsible for early warnings
ESLint plugin is responsible for checking code structure
Scan script is responsible for project-specific rules
CI is responsible for ensuring no one can skip
5. Don't block everything on day one
At the beginning of the refactoring, old Chinese is decreasing, but new requirements will continue to add more.
Governance can be done in three steps:
Step 1: Only output reports
First, tell everyone what new Chinese has been added, without blocking commits.
This phase is mainly used to adjust scan rules and reduce false positives.
Step 2: Only check changed lines
Don't require clearing all historical issues at once; only restrict new code from continuing to add technical debt.
Step 3: Block after core modules are stable
Once shared components and common patterns are ready, tighten the rules.
Blocking all 50,000 historical Chinese strings on day one will only force everyone to disable the checks.
5. How to judge if a module is truly done?
You can't just open the list page, see the buttons are in English, and consider it complete.
Acceptance is at least divided into four parts.
1. Is the copy complete?
Check:
- Page titles and route titles;
- Sidebar, breadcrumbs, and tabs;
- Form labels, placeholders, and validation;
- Table columns and filters;
- Message notifications and confirmation dialogs;
- API errors;
- Asynchronous tasks;
- Export file names;
- Text in images.
2. Do static checks pass?
At least execute:
pnpm i18n:check
Confirm:
- The current module has no unhandled Chinese;
- No missing keys;
- Interpolation parameters are consistent;
- No new display text judgments added;
- Translation results are not written into Store or cache.
It's not enough that the plugin shows "no errors"; CI must also pass.
3. Are pages and layouts normal?
Text in languages like English and Portuguese is usually longer than Chinese.
Need to actually check:
- Are buttons stretched;
- Are table headers wrapping;
- Are filter items obscured;
- Are dialogs overflowing;
- Are empty states and images correct;
- Is the page still operable on narrow screens.
Static plugins can only look at code; they cannot find these layout issues.
4. Has business behavior not changed?
This is the most easily overlooked item.
After switching languages, re-verify:
- Has the value passed to the API by the select box not changed;
- Are table row selections and button permissions normal;
- Does route navigation still use stable path or code;
- Does cache restoration still show the old language;
- Do exports and prints comply with their respective language rules;
- Can the system still function normally when CDN or language packs are missing.
The scariest thing about internationalization is not "there's still one place not translated," but "it looks like the translation succeeded, but the operational behavior has changed."
Summary
After completing this round of design, my biggest takeaway on internationalizing old projects is:
50,000 Chinese strings are just the surface workload; the real difficulty is that these texts have grown together with the business code for many years.
If you only treat it as a translation task, people will endlessly patch keys and modify JSONs, ultimately leaving many new hidden dangers.
If you treat it as an architectural refactoring, the focus shifts to:
- How to separate display text from business data;
- How shared components uniformly handle it;
- What Store and cache should save;
- What protocols dynamic menus and error APIs need;
- How to split, load, and fallback language packs;
- How multiple people can work in parallel, and how to block new issues;
- How to use plugins, scripts, CI, and page regression to check together after changes.
Once these issues are thought through, whether you specifically use Vue, React, or another framework becomes much less important.
So, facing an old project with 50,000 Chinese strings, don't rush to global search and replace.
First, find out which are just text and which have long become part of the business. The former can be translated; the latter must be separated first.
This step seems slow, but it can save you from many pitfalls that are much harder to troubleshoot later.
Top 2 of 4 from juejin.cn, machine-translated. The original thread is authoritative.
OP's article is both hardcore and solid
?
Old project = mountain of shit? [look]
[facepalm]