Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,19 @@ jobs:
run: pytest -q
- name: Demo
run: beear demo

web-i18n:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Unit tests (EN/VI chrome strings)
run: node --test packages/web/tests/i18n.test.mjs
- name: IIFE mirror present
run: |
test -f packages/web/assets/i18n.js
grep -q "BeeARI18n" packages/web/assets/i18n.js
grep -q "i18n.js" packages/web/index.html
grep -q "i18n.js" packages/web/studio3d.html
44 changes: 44 additions & 0 deletions docs/evidence-issue-8-vi-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Evidence: Vietnamese UI strings toggle (#8)

## What

EN ↔ VI chrome UI toggle for BeeAR web try-on (2D camera page + 3D studio).

## How to verify

```bash
# unit tests
node --test packages/web/tests/i18n.test.mjs

# static demo
node scripts/build-pages.mjs --no-minify
npx --yes serve site -p 4173
# open http://127.0.0.1:4173/
# click the VI button → chrome strings switch to Vietnamese
# click EN → back to English
# reload → language persists via localStorage key `beear_lang`
```

## Acceptance mapping

| Requirement | Implementation |
| --- | --- |
| EN/VI chrome strings | `packages/web/i18n.mjs` + `packages/web/assets/i18n.js` |
| Toggle button | `#btn-lang` on `index.html` and `studio3d.html` |
| Persist language | `localStorage['beear_lang']` via `BeeARI18n.loadLang/saveLang` |
| Filter labels | All / Glasses / Accessories translated |
| Empty states | Catalog empty + gallery empty translated |
| 3D studio parity | Tagline, person, frames, auto-rotate, snapshot, filters |
| Tests | `packages/web/tests/i18n.test.mjs` (CI job `web-i18n`) |

## Screenshots

- Existing VI chrome shot: `docs/screenshots/demo-vi-ui.png`
- After toggle (EN header labels): Catalog / Compare / Snapshot / Camera
- After toggle (VI header labels): Danh mục / So sánh / Chụp / Máy ảnh

## Risk notes

- Language pack is chrome UI only (SKU names remain catalog data).
- No network calls; pure client-side `localStorage`.
- Private mode: load/save lang fails soft and defaults to English.
154 changes: 154 additions & 0 deletions packages/android/beear-webview/src/main/assets/beear/assets/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**
* BeeAR chrome UI strings (EN / VI) — browser IIFE.
* Keep in sync with packages/web/i18n.mjs (unit tests).
*/
(function (g) {
const I18N = {
en: {
tagline: "Virtual try-on · glasses & accessories",
catalog: "Catalog",
pd: "PD (mm)",
pdHint: "Calibrate pupil distance for better fit scale",
select: "Select a SKU",
cam: "📷 Camera",
demo: "Demo photo",
demoNext: "Next face",
compare: "Compare",
snap: "Snapshot",
gallery: "Gallery",
person3d: "🧑 3D Person",
consentTitle: "Camera consent",
consentBody:
"BeeAR uses the camera only in your browser for try-on. Video is not uploaded to the server.",
consentOk: "Allow & start",
consentDemo: "Use demo photo",
hintIdle: "Start camera or use Demo photo · pick a frame",
hintDemo: "Photoreal demo face · PD slider · Compare A/B · Next face",
hintCam: "Camera live",
trackGeo: "tracking: geometric",
trackMp: "tracking: mediapipe",
trackDemo: "tracking: demo photo",
cloudLocal: "🔒 Local Only",
cloudServer: "☁️ Cloud Enabled",
photoConsentLabel: "Allow face photo server upload & cloud features",
filterAll: "All",
filterGlasses: "Glasses",
filterAccessory: "Accessories",
emptyFilter: "No frames match the current filter.",
emptyFilterHint: "Try adjusting filters or check your connection.",
emptyGallery: "No snapshots yet",
loadingCatalog: "Loading catalog…",
studioTagline: "Real 3D characters · glasses GLB try-on",
studioBack: "← 2D / Camera",
autoRotate: "Auto-rotate",
autoRotateOn: "Auto-rotate ON",
autoRotateOff: "Auto-rotate OFF",
person: "Person",
character: "Character",
frames3d: "Frames (3D)",
fitScale: "Fit scale",
yOffset: "Y offset",
zDepth: "Z (depth)",
loadingStudio: "Loading 3D studio…",
hintStudio: "Drag to orbit · scroll to zoom · pick person + frames",
badgeStudio: "3D try-on · GLB glasses",
loadingPerson: "Loading 3D person…",
},
vi: {
tagline: "Thử kính & phụ kiện ảo",
catalog: "Danh mục",
pd: "Khoảng đồng tử PD (mm)",
pdHint: "Hiệu chỉnh PD để scale kính chính xác hơn",
select: "Chọn mẫu",
cam: "📷 Máy ảnh",
demo: "Ảnh demo",
demoNext: "Đổi mặt",
compare: "So sánh",
snap: "Chụp",
gallery: "Thư viện",
person3d: "🧑 Nhân vật 3D",
consentTitle: "Đồng ý camera",
consentBody:
"BeeAR chỉ dùng camera trên trình duyệt để thử đồ. Video không gửi lên server.",
consentOk: "Cho phép & bắt đầu",
consentDemo: "Dùng ảnh demo",
hintIdle: "Bật camera hoặc ảnh demo · chọn khung",
hintDemo: "Ảnh người demo · chỉnh PD · So sánh · Đổi mặt",
hintCam: "Camera đang bật",
trackGeo: "tracking: hình học",
trackMp: "tracking: mediapipe",
trackDemo: "tracking: ảnh demo",
cloudLocal: "🔒 Chỉ lưu cục bộ",
cloudServer: "☁️ Đã bật Cloud",
photoConsentLabel: "Cho phép tải ảnh mặt lên server & dùng tính năng đám mây",
filterAll: "Tất cả",
filterGlasses: "Kính",
filterAccessory: "Phụ kiện",
emptyFilter: "Không có khung khớp bộ lọc hiện tại.",
emptyFilterHint: "Thử đổi bộ lọc hoặc kiểm tra kết nối.",
emptyGallery: "Chưa có ảnh chụp",
loadingCatalog: "Đang tải danh mục…",
studioTagline: "Nhân vật 3D thật · thử kính GLB",
studioBack: "← 2D / Camera",
autoRotate: "Tự xoay",
autoRotateOn: "Tự xoay BẬT",
autoRotateOff: "Tự xoay TẮT",
person: "Nhân vật",
character: "Mẫu",
frames3d: "Khung (3D)",
fitScale: "Tỷ lệ vừa",
yOffset: "Lệch Y",
zDepth: "Z (độ sâu)",
loadingStudio: "Đang tải studio 3D…",
hintStudio: "Kéo để xoay · cuộn để zoom · chọn người + khung",
badgeStudio: "Thử 3D · kính GLB",
loadingPerson: "Đang tải nhân vật 3D…",
},
};

const LANG_KEY = "beear_lang";

function normalizeLang(value) {
return value === "vi" ? "vi" : "en";
}

function t(lang, key) {
const pack = I18N[normalizeLang(lang)] || I18N.en;
return pack[key] ?? I18N.en[key] ?? key;
}

function toggleLang(lang) {
return normalizeLang(lang) === "en" ? "vi" : "en";
}

function langButtonLabel(lang) {
return normalizeLang(lang) === "en" ? "VI" : "EN";
}

function loadLang() {
try {
return normalizeLang(localStorage.getItem(LANG_KEY));
} catch (_) {
return "en";
}
}

function saveLang(lang) {
try {
localStorage.setItem(LANG_KEY, normalizeLang(lang));
} catch (_) {
/* private mode */
}
}

g.BeeARI18n = {
I18N,
LANG_KEY,
normalizeLang,
t,
toggleLang,
langButtonLabel,
loadLang,
saveLang,
};
})(typeof globalThis !== "undefined" ? globalThis : window);
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ <h2 id="lbl-catalog">Catalog</h2>

<!-- @beear/tryon core lib (shared web + Android WebView) -->
<script src="assets/beear-tryon.js"></script>
<script src="assets/i18n.js"></script>
<!-- Optional MediaPipe Face Mesh (fails soft if offline/CDN blocked) -->
<script data-src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js" crossorigin="anonymous"></script>
<!-- Optional Three.js GLB renderer (app falls back to canvas shapes if blocked) -->
Expand Down
Loading