From dfa2fc24066ce0a062e0c82b1e38b05b3cbd1738 Mon Sep 17 00:00:00 2001 From: Lectom C Han Date: Thu, 12 Feb 2026 21:25:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20i18n=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20userfront=20=EB=A1=9C=EA=B7=B8=EC=9D=B8/=EB=A1=9C=EC=BC=80?= =?UTF-8?q?=EC=9D=BC=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 18 + .gitea/workflows/code_check.yml | 205 ++-- README.md | 4 +- README_en.md | 4 +- .../components/common/LanguageSelector.tsx | 57 ++ .../src/components/layout/AppLayout.tsx | 2 + backend/internal/handler/auth_handler.go | 17 +- .../handler/auth_handler_async_test.go | 251 +++++ backend/internal/handler/user_handler.go | 44 +- .../components/common/LanguageSelector.tsx | 57 ++ devfront/src/components/layout/AppLayout.tsx | 2 + docker-compose.yaml | 10 +- docker/compose.ory.yaml | 253 ++--- docker/docker-compose.staging.template.yaml | 4 +- docker/ory/kratos/kratos.yml | 13 +- docker/ory/oathkeeper/entrypoint.sh | 8 +- docker/staging_pull_compose.template.yaml | 400 ++++++++ docs/SoT_Architecture_Policy.md | 44 + docs/i18n.md | 1 + docs/trouble-shooting/userfront-locale.md | 86 ++ locales/en.toml | 41 +- locales/ko.toml | 29 +- locales/template.toml | 25 +- scripts/sync_userfront_locales.sh | 47 + tools/i18n-scanner/report.js | 237 +++++ userfront/Dockerfile | 9 +- userfront/assets/translations/en.toml | 536 +++++++++++ userfront/assets/translations/ko.toml | 536 +++++++++++ userfront/assets/translations/template.toml | 536 +++++++++++ userfront/lib/core/i18n/locale_gate.dart | 51 + userfront/lib/core/i18n/locale_storage.dart | 7 + .../lib/core/i18n/locale_storage_stub.dart | 11 + .../lib/core/i18n/locale_storage_web.dart | 120 +++ userfront/lib/core/i18n/locale_utils.dart | 69 ++ .../lib/core/i18n/toml_asset_loader.dart | 22 + .../lib/core/services/auth_proxy_service.dart | 225 ++--- .../lib/core/services/web_window_stub.dart | 2 + .../lib/core/services/web_window_web.dart | 4 + .../lib/core/widgets/language_selector.dart | 65 ++ .../auth/presentation/error_screen.dart | 63 +- .../presentation/forgot_password_screen.dart | 50 +- .../auth/presentation/login_screen.dart | 882 ++++++++++-------- .../presentation/login_success_screen.dart | 29 +- .../auth/presentation/qr_scan_screen.dart | 33 +- .../presentation/reset_password_screen.dart | 79 +- .../auth/presentation/signup_screen.dart | 442 +++++---- .../dashboard/domain/dashboard_providers.dart | 1 - .../presentation/dashboard_screen.dart | 633 ++++++++----- .../data/repositories/profile_repository.dart | 15 +- .../presentation/pages/profile_page.dart | 266 +++--- userfront/lib/i18n.dart | 50 +- userfront/lib/main.dart | 425 +++++---- userfront/pubspec.lock | 190 +++- userfront/pubspec.yaml | 8 +- userfront/test/helpers/web_storage.dart | 5 + userfront/test/helpers/web_storage_stub.dart | 21 + userfront/test/helpers/web_storage_web.dart | 37 + userfront/test/locale_storage_web_test.dart | 88 ++ userfront/test/locale_utils_test.dart | 66 ++ userfront/test/widget_test.dart | 23 +- 60 files changed, 5724 insertions(+), 1734 deletions(-) create mode 100644 .dockerignore create mode 100644 adminfront/src/components/common/LanguageSelector.tsx create mode 100644 backend/internal/handler/auth_handler_async_test.go create mode 100644 devfront/src/components/common/LanguageSelector.tsx create mode 100644 docker/staging_pull_compose.template.yaml create mode 100644 docs/SoT_Architecture_Policy.md create mode 100644 docs/trouble-shooting/userfront-locale.md create mode 100755 scripts/sync_userfront_locales.sh create mode 100644 tools/i18n-scanner/report.js create mode 100644 userfront/assets/translations/en.toml create mode 100644 userfront/assets/translations/ko.toml create mode 100644 userfront/assets/translations/template.toml create mode 100644 userfront/lib/core/i18n/locale_gate.dart create mode 100644 userfront/lib/core/i18n/locale_storage.dart create mode 100644 userfront/lib/core/i18n/locale_storage_stub.dart create mode 100644 userfront/lib/core/i18n/locale_storage_web.dart create mode 100644 userfront/lib/core/i18n/locale_utils.dart create mode 100644 userfront/lib/core/i18n/toml_asset_loader.dart create mode 100644 userfront/lib/core/widgets/language_selector.dart create mode 100644 userfront/test/helpers/web_storage.dart create mode 100644 userfront/test/helpers/web_storage_stub.dart create mode 100644 userfront/test/helpers/web_storage_web.dart create mode 100644 userfront/test/locale_storage_web_test.dart create mode 100644 userfront/test/locale_utils_test.dart diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8d99b854 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git +.gitea +.codex +.env +.env.* +**/.dart_tool +**/.packages +**/build +**/node_modules +**/dist +**/.next +**/.cache +**/coverage +**/tmp +**/logs +**/*.log +**/*.swp +**/.DS_Store diff --git a/.gitea/workflows/code_check.yml b/.gitea/workflows/code_check.yml index 3c159b23..c296e360 100644 --- a/.gitea/workflows/code_check.yml +++ b/.gitea/workflows/code_check.yml @@ -1,118 +1,125 @@ name: Code Check on: - workflow_dispatch: - inputs: - run_lint: - description: "Run linters for Go and Flutter" - required: true - type: boolean - default: true - run_backend_tests: - description: "Run backend Go tests" - required: true - type: boolean - default: true - run_userfront_tests: - description: "Run userfront Flutter tests" - required: true - type: boolean - default: true + pull_request: + branches: + - dev + workflow_dispatch: + inputs: + run_lint: + description: "Run linters for Go and Flutter" + required: true + type: boolean + default: true + run_backend_tests: + description: "Run backend Go tests" + required: true + type: boolean + default: true + run_userfront_tests: + description: "Run userfront Flutter tests" + required: true + type: boolean + default: true jobs: - lint: - if: ${{ inputs.run_lint == true }} - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 + lint: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_lint == true }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" - - name: i18n resource check - run: | - node tools/i18n-scanner/index.js + - name: i18n resource check + run: | + mkdir -p reports + node tools/i18n-scanner/index.js + node tools/i18n-scanner/report.js + cat reports/i18n-report.txt - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: "1.25" - cache-dependency-path: backend/go.sum + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache-dependency-path: backend/go.sum - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - cache: true + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + cache: true - - name: Lint Go backend - uses: golangci/golangci-lint-action@v6 - with: - version: v1.59 - working-directory: backend - args: --enable-only=gofmt,gofumpt + - name: Lint Go backend + uses: golangci/golangci-lint-action@v6 + with: + version: v1.59 + working-directory: backend + args: --enable-only=gofmt,gofumpt - - name: Analyze Flutter userfront - run: | - cd userfront - flutter analyze --no-fatal-warnings --no-fatal-infos + - name: Analyze Flutter userfront + run: | + cd userfront + flutter analyze --no-fatal-warnings --no-fatal-infos - backend-tests: - needs: lint - if: ${{ inputs.run_backend_tests == true }} - runs-on: ubuntu-latest - services: - redis: - image: redis:7-alpine - options: > - --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 - clickhouse: - image: clickhouse/clickhouse-server:24.6 - options: > - --health-cmd "wget -qO- 'http://localhost:8123/ping'" --health-interval 10s --health-timeout 5s --health-retries 5 + backend-tests: + needs: lint + if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_backend_tests == true }} + runs-on: ubuntu-latest + services: + redis: + image: redis:7-alpine + options: > + --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + clickhouse: + image: clickhouse/clickhouse-server:24.6 + options: > + --health-cmd "wget -qO- 'http://localhost:8123/ping'" --health-interval 10s --health-timeout 5s --health-retries 5 - env: - REDIS_ADDR: redis:6379 - CLICKHOUSE_HOST: clickhouse - CLICKHOUSE_PORT_NATIVE: 9000 + env: + REDIS_ADDR: redis:6379 + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_PORT_NATIVE: 9000 - steps: - - name: Checkout code - uses: actions/checkout@v4 + steps: + - name: Checkout code + uses: actions/checkout@v4 - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: "1.25" - cache-dependency-path: backend/go.sum + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.25" + cache-dependency-path: backend/go.sum - - name: Run backend tests - run: | - cd backend - go test -v ./... + - name: Run backend tests + run: | + cd backend + go test -v ./... - userfront-tests: - needs: lint - if: ${{ inputs.run_userfront_tests == true }} - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 + userfront-tests: + needs: lint + if: ${{ github.event_name != 'workflow_dispatch' || inputs.run_userfront_tests == true }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - cache: true + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + cache: true - - name: Run userfront tests - run: | - cd userfront - if [ -d test ]; then - flutter test - else - echo "No userfront tests: skipping (test/ directory not found)." - fi + - name: Run userfront tests + run: | + cd userfront + if [ -d test ]; then + flutter test + flutter test --platform chrome test/locale_storage_web_test.dart + else + echo "No userfront tests: skipping (test/ directory not found)." + fi diff --git a/README.md b/README.md index 7a870483..5268d366 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ flowchart - userfront가 바라보는 backend ### 2. UserFront(Flutter Web/App) -- **Framework**: Flutter 3.32+ +- **Framework**: Flutter 3.38.0+ - **Key Packages**: `flutter_riverpod`, `go_router` - **Features**: - 탭 기반 로그인 UI (비밀번호 기반 / 링크 기반 / QR 기반 등) @@ -147,7 +147,7 @@ Kratos가 사용자 SoT이며 Hydra는 순수 OIDC 토큰 엔진입니다. 비 ### 사전 요구사항 (Prerequisites) - Docker & Docker Compose -- Flutter SDK (로컬 개발용) +- Flutter SDK (로컬 개발용, 3.38.0+) - Go (로컬 백엔드 개발용) ### 환경 설정 (Environment Setup) diff --git a/README_en.md b/README_en.md index 8d54e892..5ba1130b 100644 --- a/README_en.md +++ b/README_en.md @@ -6,7 +6,7 @@ It leverages **Descope** for secure, passwordless authentication (Enchanted Link ## 🏗 Architecture ### 1. Frontend (Flutter Web) -- **Framework**: Flutter 3.32+ +- **Framework**: Flutter 3.38.0+ - **Organization**: `kr.co.baroncs` - **Key Packages**: `descope`, `flutter_riverpod`, `go_router` - **Features**: @@ -32,7 +32,7 @@ It leverages **Descope** for secure, passwordless authentication (Enchanted Link ### Prerequisites - Docker & Docker Compose -- Flutter SDK (for local development) +- Flutter SDK (for local development, 3.38.0+) - Go (for local backend development) ### Environment Setup diff --git a/adminfront/src/components/common/LanguageSelector.tsx b/adminfront/src/components/common/LanguageSelector.tsx new file mode 100644 index 00000000..fe77df72 --- /dev/null +++ b/adminfront/src/components/common/LanguageSelector.tsx @@ -0,0 +1,57 @@ +import { useState } from "react"; +import { t } from "../../lib/i18n"; + +const LOCALE_STORAGE_KEY = "locale"; +const SUPPORTED_LOCALES = ["ko", "en"] as const; + +type Locale = (typeof SUPPORTED_LOCALES)[number]; + +function resolveLocale(): Locale { + if (typeof window === "undefined") { + return "ko"; + } + + const stored = window.localStorage.getItem(LOCALE_STORAGE_KEY); + if (stored === "ko" || stored === "en") { + return stored; + } + + const pathLocale = window.location.pathname.split("/")[1]; + if (pathLocale === "ko" || pathLocale === "en") { + return pathLocale; + } + + const browserLang = window.navigator.language.toLowerCase(); + return browserLang.startsWith("ko") ? "ko" : "en"; +} + +function LanguageSelector() { + const [locale, setLocale] = useState(resolveLocale()); + + const handleChange = (next: Locale) => { + if (next === locale) { + return; + } + window.localStorage.setItem(LOCALE_STORAGE_KEY, next); + setLocale(next); + window.location.reload(); + }; + + return ( + + ); +} + +export default LanguageSelector; diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx index c9ece4f8..14f68839 100644 --- a/adminfront/src/components/layout/AppLayout.tsx +++ b/adminfront/src/components/layout/AppLayout.tsx @@ -13,6 +13,7 @@ import { import { useEffect, useState } from "react"; import { NavLink, Outlet } from "react-router-dom"; import { t } from "../../lib/i18n"; +import LanguageSelector from "../common/LanguageSelector"; import RoleSwitcher from "./RoleSwitcher"; const navItems = [ @@ -132,6 +133,7 @@ function AppLayout() {
+
+