Bản địa hoá React Intl và ICU: Trích xuất, đồng bộ và đánh giá với Hyperlocalise
Tích hợp react-intl và cú pháp thông điệp ICU vào quy trình làm việc tích hợp sẵn trong repo—trích xuất danh mục bằng Hyperlocalise CLI, xác thực số nhiều trong các pull request, rà soát trên Hyperlocalise và đưa JSON đã dịch vào môi trường production.
React Intl lưu trữ nội dung hiển thị cho người dùng trong TypeScript, nhưng người dịch và CI cần một danh mục ổn định trên đĩa. Cú pháp ICU—số nhiều, lựa chọn, số và ngày tháng—phải được giữ nguyên trong quá trình chuyển giao đó để không gây lỗi khi chạy.
Hướng dẫn này trình bày cách kết nối react-intl, ICU và hyperlocalise CLI thành một quy trình làm việc:
- Engineers write messages in
defineMessagesand<FormattedMessage />. hl extractcập nhật lại danh mục FormatJS tiếng Anh từ nguồn.- GitHub kiểm tra pull request để phát hiện sai lệch, khóa bị thiếu và lỗi cấu trúc ICU.
hl sync pushgửi danh mục đến Hyperlocalise để xem xét.- và
hl sync pullvàhl packđưa các bản dịch đã được duyệt trở lạilang/*.jsoncho ứng dụng của bạn.
Mẫu này phản ánh cách Hyperlocalise tự sử dụng ứng dụng web của mình. Đối với ghi chú phát hành và các tệp JSON không dùng React trong cùng kho lưu trữ, hãy kết hợp hướng dẫn này với quy trình bản địa hóa trên GitHub từ pull request đến bản phát hành đa ngôn ngữ.
Chúng ta sẽ xây dựng gì
Giả sử ứng dụng Next.js hoặc Vite React có cấu trúc như sau:
.
├── .github/workflows/
│ └── localise.yml
├── src/
│ ├── components/
│ │ └── saved-filters-banner.messages.ts
│ └── app/
│ └── filters-page.tsx
├── lang/
│ ├── en-US.json # extracted source catalog (FormatJS shape)
│ ├── fr-FR.json
│ └── de-DE.json
└── i18n.yml
Tiếng Anh (en-US) là ngôn ngữ nguồn. Tiếng Pháp và tiếng Đức là ngôn ngữ đích. ID thông điệp và giá trị defaultMessage nằm trong các tệp *.messages.ts (mô-đun phía client) và đôi khi trong các descriptor nội tuyến. JSON đã trích xuất là định dạng Hyperlocalise đồng bộ hóa; nhiều ứng dụng nhập JSON đã đóng gói khi chạy.
Bạn sẽ cần:
- một dự án Hyperlocalise với
en-USlàm ngôn ngữ nguồn và các ngôn ngữ đích của bạn; HYPERLOCALISE_API_KEYvàHYPERLOCALISE_PROJECT_IDdưới dạng secrets của GitHub Actions; vàreact-intl(hoặc@formatjs/intl) đã được cài đặt trong ứng dụng.
Bước 1: viết các thông điệp react-intl có hỗ trợ ICU
Đặt nội dung sản phẩm trong bộ mô tả thông điệp, không rải rác dưới dạng chuỗi ký tự cố định. Dùng ID tường minh để việc trích xuất và duyệt vẫn ổn định khi thay đổi câu chữ.
src/components/saved-filters-banner.messages.ts:
"use client";
import { defineMessages } from "react-intl";
export const savedFiltersBannerMessages = defineMessages({
title: {
id: "filters.banner.title",
defaultMessage: "Saved filters",
description: "Heading above the saved-filters list",
},
savedCount: {
id: "filters.banner.savedCount",
defaultMessage:
"{count, plural, =0 {No saved filters yet} one {# saved filter} other {# saved filters}}",
description: "Banner summary of how many filters the user saved",
},
scope: {
id: "filters.banner.scope",
defaultMessage:
"{scope, select, workspace {Shared with your workspace} personal {Only visible to you} other {Custom scope}}",
description: "Explains who can see the saved filter set",
},
});
Sử dụng ICU bên trong defaultMessage khi nội dung phụ thuộc vào số hoặc enum. React Intl đánh giá toàn bộ thông điệp khi chạy; người dịch phải giữ nguyên các cấu trúc {count, plural, ...} và {scope, select, ...}, đồng thời thay đổi các nhánh hiển thị bằng ngôn ngữ tự nhiên.
In a page component, pass ICU values through formatMessage or <FormattedMessage />:
"use client";
import { FormattedMessage, useIntl } from "react-intl";
import { savedFiltersBannerMessages } from "../components/saved-filters-banner.messages";
export function FiltersPage({ savedCount, scope }: { savedCount: number; scope: string }) {
const intl = useIntl();
return (
<section>
<h1>
<FormattedMessage {...savedFiltersBannerMessages.title} />
</h1>
<p>{intl.formatMessage(savedFiltersBannerMessages.savedCount, { count: savedCount })}</p>
<p>{intl.formatMessage(savedFiltersBannerMessages.scope, { scope })}</p>
</section>
);
}
Thành phần máy chủ: không import *.messages.ts từ các mô-đun chỉ dành cho máy chủ—defineMessages chỉ dành cho client. Hãy đánh dấu giao diện người dùng là "use client" hoặc dùng các đối tượng { id, defaultMessage, description } nội tuyến với getIntlShape(locale).formatMessage() trên máy chủ. Xem các ranh giới react-intl trong framework của bạn; bước trích xuất vẫn tìm thấy các descriptor trong những tệp .ts và .tsx mà nó quét.
Tránh --flatten trên các thông điệp ICU mà bạn định phát hành dưới dạng các đơn vị react-intl riêng lẻ. Việc làm phẳng đưa các nhánh số nhiều và lựa chọn lên cấp cao hơn để phục vụ quy trình dịch chuyên biệt; đây không phải là tùy chọn mặc định cho danh mục thời gian chạy.
Bước 2: ánh xạ các danh mục trong i18n.yml
Tạo i18n.yml ở thư mục gốc của kho lưu trữ (hoặc trong thư mục ứng dụng nếu monorepo lưu cấu hình bên cạnh giao diện người dùng):
version: hyperlocalise@1.13.10
locales:
source: en-US
targets:
- fr-FR
- de-DE
buckets:
ui:
files:
- from: lang/{{source}}.json
to: lang/{{target}}.json
llm:
profiles:
default:
provider: openai
model: gpt-6-luna
hyperlocalise:
project_id_env: HYPERLOCALISE_PROJECT_ID
api_base_url: https://hyperlocalise.com/api
api_key_env: HYPERLOCALISE_API_KEY
Hyperlocalise coi JSON FormatJS là nội dung hạng nhất: mỗi khóa là một id thông báo, mỗi giá trị bao gồm defaultMessage và description tùy chọn. Các chuỗi ICU luôn là một giá trị cho mỗi id—run, check và đồng bộ hóa không tách chúng thành các câu.
Cố định phiên bản CLI trong i18n.yml (hoặc cố định phiên bản của tác vụ cài đặt) để các máy cục bộ và GitHub Actions chạy cùng một bộ trích xuất và bộ xác thực.
Bước 3: trích xuất danh mục nguồn bằng CLI
Từ thư mục chứa i18n.yml, hãy làm mới danh mục tiếng Anh:
export HYPERLOCALISE_API_KEY="your-api-key"
export HYPERLOCALISE_PROJECT_ID="your-project-id"
hl extract src \
--out-file lang/en-US.json \
--ignore "**/*.test.ts" \
--ignore "**/*.test.tsx" \
--ignore "**/*.stories.tsx" \
--ignore "**/__tests__/**"
extract quét .ts và .tsx để tìm các bộ mô tả trong:
defineMessage/defineMessagesintl.formatMessage(...)<FormattedMessage ... />
Nó tạo JSON FormatJS tuân thủ nghiêm ngặt:
{
"filters.banner.savedCount": {
"defaultMessage": "{count, plural, =0 {No saved filters yet} one {# saved filter} other {# saved filters}}",
"description": "Banner summary of how many filters the user saved"
}
}
Nếu một bộ mô tả bỏ qua id, CLI sẽ tạo một mã băm tương thích với FormatJS từ defaultMessage và description. Các ID được chỉ định rõ ràng sẽ dễ xem xét hơn trong các bản diff và trong Hyperlocalise.
Commit lang/en-US.json cùng với thay đổi mã. Hãy xem việc thiếu commit trích xuất giống như thiếu migration: nền tảng sẽ không thấy các chuỗi mới cho đến khi catalog được cập nhật.
Tùy chọn: --prefix-id thêm đường dẫn tệp đã chuẩn hóa làm tiền tố cho các ID (src.components.saved-filters-banner.title). Kết hợp với hl pack --prefix-id khi các gói runtime cần ID ngắn. Các ví dụ ở đây sử dụng ID logic ổn định.
Bước 4: bảo vệ pull request bằng extract và check
Thêm .github/workflows/localise.yml:
name: Localise
on:
pull_request:
paths:
- "i18n.yml"
- "lang/**"
- "src/**/*.ts"
- "src/**/*.tsx"
- ".github/workflows/localise.yml"
push:
branches: [main]
paths:
- "i18n.yml"
- "lang/en-US.json"
- "src/**/*.ts"
- "src/**/*.tsx"
workflow_dispatch:
inputs:
pull_translations:
description: Pull reviewed translations into a pull request
required: true
type: boolean
default: true
concurrency:
group: localise-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: false
jobs:
check:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@v4
- name: Install Hyperlocalise
uses: hyperlocalise/hyperlocalise/install@v1
with:
version: config
- name: Verify extracted catalog matches source
run: |
set -euo pipefail
hl extract src \
--out-file lang/en-US.json \
--ignore "**/*.test.ts" \
--ignore "**/*.test.tsx" \
--ignore "**/*.stories.tsx" \
--ignore "**/__tests__/**"
git diff --exit-code -- lang/en-US.json
- name: Check localisation content
uses: hyperlocalise/hyperlocalise@v1
with:
check: check
config-path: i18n.yml
hyperlocalise-version: config
github-diff: true
fail-on-findings: true
upload-artifact: true
Hai cổng phối hợp với nhau:
- Sai lệch trong trích xuất — nếu ai đó chỉnh sửa
defaultMessagetrong mã nhưng quênhl extract, tác vụ sẽ thất bại trêngit diff. hyperlocalise check— cùng vớigithub-diff: true, xác thực các khóa đã thay đổi tronglang/en-US.jsonvà các mục tiêu để tìm các vấn đề nhưnot_localized,placeholder_mismatchvàicu_shape_mismatch.
Bước kiểm tra cuối cùng đó rất quan trọng với ICU: một chuỗi tiếng Pháp làm mất {count, plural, ...} hoặc đảo thứ tự các nhánh có thể trông vẫn ổn với người chỉ đọc lướt JSON, nhưng sẽ gây lỗi khi chạy. Phát hiện sai lệch cấu trúc trong CI sẽ ít tốn kém hơn so với phát hiện khi đã đưa vào môi trường production.
Chạy các bước kiểm tra tương tự trên máy cục bộ trước khi đẩy:
hl extract src --out-file lang/en-US.json --ignore "**/*.test.ts" --ignore "**/*.test.tsx"
git diff --exit-code -- lang/en-US.json
hl check --bucket ui
Bước 5: đẩy danh mục đã trích xuất lên sau khi hợp nhất
Thêm một tác vụ push vào cùng quy trình làm việc:
push-sources:
if: github.event_name == 'push'
runs-on: ubuntu-latest
environment: localisation
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Install Hyperlocalise
uses: hyperlocalise/hyperlocalise/install@v1
with:
version: config
- name: Refresh source catalog
run: |
hl extract src \
--out-file lang/en-US.json \
--ignore "**/*.test.ts" \
--ignore "**/*.test.tsx" \
--ignore "**/*.stories.tsx" \
--ignore "**/__tests__/**"
- name: Push sources
run: hl sync push
env:
HYPERLOCALISE_API_KEY: ${{ secrets.HYPERLOCALISE_API_KEY }}
HYPERLOCALISE_PROJECT_ID: ${{ secrets.HYPERLOCALISE_PROJECT_ID }}
Sau khi hợp nhất, hl sync push tải lang/en-US.json lên dự án Hyperlocalise được liên kết. Chạy lại extract trên main giúp tránh tình trạng chạy đua khi mã được hợp nhất mà không có catalog tương ứng trong Git.
Sử dụng hl sync push --dry-run khi bạn thay đổi đường dẫn bucket hoặc danh sách ngôn ngữ.
Bước 6: xem lại các thông báo ICU trong Hyperlocalise
Người dịch nên xem toàn bộ thông điệp ICU, chứ không phải các đoạn tiếng Anh riêng lẻ. Khi rà soát, hãy đặt những câu hỏi đặc thù theo ngôn ngữ mà ICU che khuất trong một chuỗi duy nhất:
| Tin nhắn | Câu hỏi đánh giá |
|---|---|
filters.banner.savedCount |
Các nhánh =0, one và other có tự nhiên không? # có mở rộng chính xác theo quy tắc số nhiều của từng ngôn ngữ không? |
filters.banner.scope |
select có bao quát mọi giá trị scope mà ứng dụng gửi không? other có phải là phương án dự phòng an toàn không? |
| Nhãn ngắn | Các chuỗi đã dịch vẫn vừa với các nút sau khi mở rộng số nhiều chứ? |
Đính kèm ảnh chụp màn hình khi một nhánh số nhiều xuất hiện trong bố cục bị giới hạn. Hyperlocalise lưu giữ bảng thuật ngữ và hướng dẫn dự án cùng với phân đoạn — CLI chỉ di chuyển các tệp.
Phê duyệt bản dịch trên nền tảng trước khi đồng bộ chúng về. Việc phê duyệt là cổng kiểm soát ngôn ngữ; Git ghi nhận những gì thực sự được phát hành.
Bước 7: lấy bản dịch và đóng gói để sử dụng trong thời gian chạy
Thêm một công việc kéo thủ công:
pull-translations:
if: github.event_name == 'workflow_dispatch' && inputs.pull_translations
runs-on: ubuntu-latest
environment: localisation
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Install Hyperlocalise
uses: hyperlocalise/hyperlocalise/install@v1
with:
version: config
- name: Refresh source catalog
run: |
hl extract src --out-file lang/en-US.json \
--ignore "**/*.test.ts" \
--ignore "**/*.test.tsx"
- name: Pull reviewed translations
run: hl sync pull
env:
HYPERLOCALISE_API_KEY: ${{ secrets.HYPERLOCALISE_API_KEY }}
HYPERLOCALISE_PROJECT_ID: ${{ secrets.HYPERLOCALISE_PROJECT_ID }}
- name: Pack locale catalogs
run: hl pack --bucket ui
- name: Create translation pull request
uses: peter-evans/create-pull-request@v8
with:
branch: hyperlocalise/reviewed-translations
delete-branch: true
commit-message: "chore(i18n): sync reviewed react-intl catalogs"
title: "chore(i18n): sync reviewed react-intl catalogs"
body: |
Pulls reviewed UI strings from Hyperlocalise.
- `hl sync pull` — download target locale JSON
- `hl pack` — strip translator metadata, keep id → defaultMessage
Verify ICU placeholders, plural branches, and layout in fr-FR and de-DE before merging.
labels: localization
sync pull ghi lang/fr-FR.json và lang/de-DE.json theo cấu trúc FormatJS (id, defaultMessage, đôi khi description). hl pack xóa description và các siêu dữ liệu khác, đồng thời giữ nguyên ICU trong từng defaultMessage—sẵn sàng cho các bundler nhập JSON theo từng locale.
Ví dụ về mục tiếng Pháp được đóng gói:
{
"filters.banner.savedCount": {
"defaultMessage": "{count, plural, =0 {Aucun filtre enregistré} one {# filtre enregistré} other {# filtres enregistrés}}"
}
}
Mở pull request bản dịch trong ứng dụng, chuyển đổi ngôn ngữ và kiểm tra count = 0, count = 1 và count = 5. Các lỗi hồi quy ICU thường chỉ xuất hiện với quy tắc số nhiều của các ngôn ngữ không phải tiếng Anh.
Bước 8: tải danh mục trong ứng dụng
Nhập các tệp ngôn ngữ được đóng gói và ánh xạ chúng vào IntlProvider hoặc createIntl:
import frFR from "../lang/fr-FR.json";
import deDE from "../lang/de-DE.json";
const catalogs = {
"fr-FR": flattenFormatJSCatalog(frFR),
"de-DE": flattenFormatJSCatalog(deDE),
};
function flattenFormatJSCatalog(
catalog: Record<string, { defaultMessage: string } | string>,
): Record<string, string> {
return Object.fromEntries(
Object.entries(catalog).map(([id, value]) => [
id,
typeof value === "string" ? value : value.defaultMessage,
]),
);
}
Một số nhóm chỉ giữ các giá trị mặc định tiếng Anh trong mã nguồn và chỉ tải JSON cho các ngôn ngữ đích—cả hai cách đều hiệu quả nếu defaultMessage trong mã và lang/en-US.json luôn đồng bộ thông qua quá trình trích xuất.
Cách hoạt động của toàn bộ quy trình
feature branch
│
├─ edit *.messages.ts / FormattedMessage
├─ hl extract → lang/en-US.json
└─ pull request
├─ extract drift check
└─ hyperlocalise check (ICU + keys, diff-scoped)
│
▼
main
│
├─ hl extract
└─ hl sync push → Hyperlocalise
│
├─ translate + review ICU
└─ approve
│
▼
workflow_dispatch
│
├─ hl sync pull
├─ hl pack
└─ translation pull request
│
└─ merge → deploy
Extract kết nối mã nguồn với danh mục. Sync kết nối danh mục với người đánh giá. Pack kết nối JSON đã được duyệt với gói của bạn.
Các lỗi thường gặp
Yêu cầu kéo bị lỗi do trích xuất bị lệch hướng
Chạy hl extract trên máy cục bộ với cùng các mẫu --ignore như CI, commit lang/en-US.json rồi push. Nếu ID nhảy bất thường, hãy xác nhận các descriptor có chứa các trường id ổn định.
icu_shape_mismatch trong một bản dịch vốn dĩ “tốt”
So sánh thứ tự nhánh và tên placeholder với en-US. Chạy hl check --check icu_shape_mismatch --locale fr-FR trên máy cục bộ. Sửa JSON đích hoặc gửi lại đoạn này để xem xét—đừng bỏ qua bước kiểm tra đối với các thông điệp ICU thực sự.
Thời gian chạy hiển thị MISSING_TRANSLATION hoặc tiếng Anh trong ngôn ngữ đích
Xác nhận pull request bản dịch đã được hợp nhất, hl pack đã chạy và các lệnh import trỏ đến các tệp đã đóng gói. Xác minh ID thông điệp trong mã khớp với các khóa trong JSON (bao gồm cả quy ước --prefix-id).
hl sync pull không thay đổi gì
Xác nhận các phê duyệt trong dự án được tham chiếu bởi HYPERLOCALISE_PROJECT_ID. Chạy hl sync pull --dry-run. Đảm bảo các đường dẫn i18n.yml to: khớp với nơi ứng dụng nhập các catalog.
Các tệp đã đóng gói vô tình bị loại bỏ ICU.
Dùng hl pack mặc định trên JSON FormatJS — nó giữ nguyên defaultMessage. Đừng chạy pack với các quy trình dành cho JSON lồng nhau thông thường, trừ khi đó là cấu trúc catalog của bạn.
Danh sách kiểm tra phát hành
Trước khi phát hành một tính năng phụ thuộc vào nội dung mới:
- Các bộ mô tả thông điệp được hợp nhất với
lang/en-US.json - Trích xuất pull request và
hyperlocalise checkđã vượt qua. -
hl sync pushđã chạy vàomain - Đã xem xét và phê duyệt các ngôn ngữ mục tiêu trong Hyperlocalise
- Yêu cầu kéo bản dịch đã được hợp nhất (
sync pull+pack) - Kiểm thử QA thủ công các nhánh số nhiều và
selecttheo từng ngôn ngữ - Bản triển khai production sử dụng các artifact đã hợp nhất
lang/*.json
Luôn cập nhật thông tin cho bản trích xuất.
React Intl khuyến khích đặt nội dung ngay cạnh mã nguồn; Hyperlocalise khuyến khích các bản dịch được duyệt và lưu trong tệp. Lệnh hl extract kết nối hai thế giới đó mà không cần dùng một CLI FormatJS riêng biệt cho các danh mục cơ bản.
Dùng check để bảo toàn cấu trúc ICU trong các yêu cầu kéo. Dùng đồng bộ cho quy trình làm việc của người đánh giá. Dùng pack để giữ cho các gói production gọn nhẹ trong khi người dịch vẫn lưu giữ siêu dữ liệu phong phú trong Git giữa các lần kéo.
Để có cái nhìn toàn diện hơn về quy trình phát hành GitHub—bao gồm cả ghi chú phát hành Markdown bên cạnh các chuỗi giao diện người dùng—hãy tiếp tục với Quy trình bản địa hóa GitHub: từ pull request đến bản phát hành đa ngôn ngữ, hoặc khám phá bản địa hóa sản phẩm trên Hyperlocalise.
