docs: add comprehensive K6 load testing guide with API structure
- Document all API endpoints (auth, listings, payments, search) - Include DTOs and request/response body shapes - Document authentication methods and rate limits - Provide database and environment configuration - Include existing test setup (Playwright, Vitest) - Detail CI/CD pipeline structure - Recommend K6 endpoints and test patterns - Provide file location references for quick lookup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
134
.github/dependabot.yml
vendored
Normal file
134
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
# ── Node.js / pnpm dependencies ──────────────────────────────────
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "06:00"
|
||||||
|
timezone: "Asia/Ho_Chi_Minh"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
reviewers:
|
||||||
|
- "goodgo/platform-team"
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "security"
|
||||||
|
# Group minor/patch updates to reduce PR noise
|
||||||
|
groups:
|
||||||
|
dev-dependencies:
|
||||||
|
patterns:
|
||||||
|
- "@types/*"
|
||||||
|
- "eslint*"
|
||||||
|
- "prettier*"
|
||||||
|
- "typescript*"
|
||||||
|
- "vitest*"
|
||||||
|
- "@playwright/*"
|
||||||
|
- "husky"
|
||||||
|
- "lint-staged"
|
||||||
|
- "tsx"
|
||||||
|
- "turbo"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
nestjs:
|
||||||
|
patterns:
|
||||||
|
- "@nestjs/*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
prisma:
|
||||||
|
patterns:
|
||||||
|
- "prisma"
|
||||||
|
- "@prisma/*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
# Security updates always get individual PRs (not grouped)
|
||||||
|
commit-message:
|
||||||
|
prefix: "deps"
|
||||||
|
include: "scope"
|
||||||
|
|
||||||
|
# ── Python dependencies (AI services) ────────────────────────────
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/libs/ai-services"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "06:00"
|
||||||
|
timezone: "Asia/Ho_Chi_Minh"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "security"
|
||||||
|
- "ai-services"
|
||||||
|
commit-message:
|
||||||
|
prefix: "deps(ai)"
|
||||||
|
include: "scope"
|
||||||
|
|
||||||
|
# ── GitHub Actions ───────────────────────────────────────────────
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "06:00"
|
||||||
|
timezone: "Asia/Ho_Chi_Minh"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "ci"
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
commit-message:
|
||||||
|
prefix: "ci"
|
||||||
|
include: "scope"
|
||||||
|
|
||||||
|
# ── Docker base images ──────────────────────────────────────────
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/apps/api"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "06:00"
|
||||||
|
timezone: "Asia/Ho_Chi_Minh"
|
||||||
|
open-pull-requests-limit: 3
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "docker"
|
||||||
|
commit-message:
|
||||||
|
prefix: "docker(api)"
|
||||||
|
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/apps/web"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "06:00"
|
||||||
|
timezone: "Asia/Ho_Chi_Minh"
|
||||||
|
open-pull-requests-limit: 3
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "docker"
|
||||||
|
commit-message:
|
||||||
|
prefix: "docker(web)"
|
||||||
|
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/libs/ai-services"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
time: "06:00"
|
||||||
|
timezone: "Asia/Ho_Chi_Minh"
|
||||||
|
open-pull-requests-limit: 3
|
||||||
|
labels:
|
||||||
|
- "dependencies"
|
||||||
|
- "docker"
|
||||||
|
commit-message:
|
||||||
|
prefix: "docker(ai)"
|
||||||
61
.github/workflows/codeql.yml
vendored
Normal file
61
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
name: CodeQL Analysis
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
schedule:
|
||||||
|
# Run weekly on Monday at 06:17 UTC — off-peak to avoid :00/:30 congestion
|
||||||
|
- cron: "17 6 * * 1"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: codeql-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: CodeQL (${{ matrix.language }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [javascript-typescript]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v3
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# Use extended security queries for deeper analysis
|
||||||
|
queries: security-extended,security-and-quality
|
||||||
|
config: |
|
||||||
|
paths:
|
||||||
|
- apps/
|
||||||
|
- libs/
|
||||||
|
paths-ignore:
|
||||||
|
- node_modules/
|
||||||
|
- "**/dist/"
|
||||||
|
- "**/*.spec.ts"
|
||||||
|
- "**/*.test.ts"
|
||||||
|
- "**/__tests__/"
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v3
|
||||||
|
with:
|
||||||
|
category: "/language:${{ matrix.language }}"
|
||||||
|
# SARIF results are automatically uploaded to GitHub Security tab
|
||||||
|
upload: always
|
||||||
312
.github/workflows/security.yml
vendored
Normal file
312
.github/workflows/security.yml
vendored
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
name: Security Scanning
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
schedule:
|
||||||
|
# Run daily at 05:43 UTC — catch new CVEs early
|
||||||
|
- cron: "43 5 * * *"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: security-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ── Dependency Audit ─────────────────────────────────────────────
|
||||||
|
dependency-audit:
|
||||||
|
name: Dependency Audit (pnpm)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run pnpm audit
|
||||||
|
run: |
|
||||||
|
echo "## Dependency Audit Report" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Run audit, capture output and exit code
|
||||||
|
set +e
|
||||||
|
AUDIT_OUTPUT=$(pnpm audit --json 2>&1)
|
||||||
|
AUDIT_EXIT=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Parse and display summary
|
||||||
|
echo "$AUDIT_OUTPUT" | jq -r '
|
||||||
|
if .metadata then
|
||||||
|
"| Severity | Count |\n|----------|-------|\n" +
|
||||||
|
"| Critical | \(.metadata.vulnerabilities.critical // 0) |\n" +
|
||||||
|
"| High | \(.metadata.vulnerabilities.high // 0) |\n" +
|
||||||
|
"| Moderate | \(.metadata.vulnerabilities.moderate // 0) |\n" +
|
||||||
|
"| Low | \(.metadata.vulnerabilities.low // 0) |\n" +
|
||||||
|
"| Info | \(.metadata.vulnerabilities.info // 0) |"
|
||||||
|
else
|
||||||
|
"No vulnerabilities found ✅"
|
||||||
|
end
|
||||||
|
' >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "Audit completed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Also run human-readable output
|
||||||
|
pnpm audit 2>&1 || true
|
||||||
|
|
||||||
|
# Fail on critical or high vulnerabilities only
|
||||||
|
pnpm audit --audit-level=critical
|
||||||
|
continue-on-error: false
|
||||||
|
|
||||||
|
# ── Container Scanning (Trivy) ───────────────────────────────────
|
||||||
|
trivy-api:
|
||||||
|
name: Trivy Scan — API Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build API image for scanning
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: apps/api/Dockerfile
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: goodgo-api:scan
|
||||||
|
cache-from: type=gha,scope=api-scan
|
||||||
|
cache-to: type=gha,mode=max,scope=api-scan
|
||||||
|
|
||||||
|
- name: Run Trivy vulnerability scanner (API)
|
||||||
|
uses: aquasecurity/trivy-action@0.28.0
|
||||||
|
with:
|
||||||
|
image-ref: "goodgo-api:scan"
|
||||||
|
format: "sarif"
|
||||||
|
output: "trivy-api-results.sarif"
|
||||||
|
severity: "CRITICAL,HIGH"
|
||||||
|
# Ignore unfixed vulns to reduce noise
|
||||||
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
- name: Upload Trivy SARIF (API)
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
sarif_file: "trivy-api-results.sarif"
|
||||||
|
category: "trivy-api"
|
||||||
|
|
||||||
|
- name: Trivy table output (API)
|
||||||
|
uses: aquasecurity/trivy-action@0.28.0
|
||||||
|
with:
|
||||||
|
image-ref: "goodgo-api:scan"
|
||||||
|
format: "table"
|
||||||
|
severity: "CRITICAL,HIGH,MEDIUM"
|
||||||
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
trivy-web:
|
||||||
|
name: Trivy Scan — Web Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build Web image for scanning
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: apps/web/Dockerfile
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: goodgo-web:scan
|
||||||
|
cache-from: type=gha,scope=web-scan
|
||||||
|
cache-to: type=gha,mode=max,scope=web-scan
|
||||||
|
|
||||||
|
- name: Run Trivy vulnerability scanner (Web)
|
||||||
|
uses: aquasecurity/trivy-action@0.28.0
|
||||||
|
with:
|
||||||
|
image-ref: "goodgo-web:scan"
|
||||||
|
format: "sarif"
|
||||||
|
output: "trivy-web-results.sarif"
|
||||||
|
severity: "CRITICAL,HIGH"
|
||||||
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
- name: Upload Trivy SARIF (Web)
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
sarif_file: "trivy-web-results.sarif"
|
||||||
|
category: "trivy-web"
|
||||||
|
|
||||||
|
- name: Trivy table output (Web)
|
||||||
|
uses: aquasecurity/trivy-action@0.28.0
|
||||||
|
with:
|
||||||
|
image-ref: "goodgo-web:scan"
|
||||||
|
format: "table"
|
||||||
|
severity: "CRITICAL,HIGH,MEDIUM"
|
||||||
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
trivy-ai:
|
||||||
|
name: Trivy Scan — AI Services Image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build AI Services image for scanning
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./libs/ai-services
|
||||||
|
file: libs/ai-services/Dockerfile
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: goodgo-ai:scan
|
||||||
|
cache-from: type=gha,scope=ai-scan
|
||||||
|
cache-to: type=gha,mode=max,scope=ai-scan
|
||||||
|
|
||||||
|
- name: Run Trivy vulnerability scanner (AI)
|
||||||
|
uses: aquasecurity/trivy-action@0.28.0
|
||||||
|
with:
|
||||||
|
image-ref: "goodgo-ai:scan"
|
||||||
|
format: "sarif"
|
||||||
|
output: "trivy-ai-results.sarif"
|
||||||
|
severity: "CRITICAL,HIGH"
|
||||||
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
- name: Upload Trivy SARIF (AI)
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
sarif_file: "trivy-ai-results.sarif"
|
||||||
|
category: "trivy-ai"
|
||||||
|
|
||||||
|
- name: Trivy table output (AI)
|
||||||
|
uses: aquasecurity/trivy-action@0.28.0
|
||||||
|
with:
|
||||||
|
image-ref: "goodgo-ai:scan"
|
||||||
|
format: "table"
|
||||||
|
severity: "CRITICAL,HIGH,MEDIUM"
|
||||||
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
# ── Filesystem / IaC Scanning ────────────────────────────────────
|
||||||
|
trivy-fs:
|
||||||
|
name: Trivy Filesystem Scan
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run Trivy filesystem scanner
|
||||||
|
uses: aquasecurity/trivy-action@0.28.0
|
||||||
|
with:
|
||||||
|
scan-type: "fs"
|
||||||
|
scan-ref: "."
|
||||||
|
format: "sarif"
|
||||||
|
output: "trivy-fs-results.sarif"
|
||||||
|
severity: "CRITICAL,HIGH"
|
||||||
|
ignore-unfixed: true
|
||||||
|
scanners: "vuln,secret,misconfig"
|
||||||
|
|
||||||
|
- name: Upload Trivy SARIF (filesystem)
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
sarif_file: "trivy-fs-results.sarif"
|
||||||
|
category: "trivy-filesystem"
|
||||||
|
|
||||||
|
- name: Trivy filesystem table output
|
||||||
|
uses: aquasecurity/trivy-action@0.28.0
|
||||||
|
with:
|
||||||
|
scan-type: "fs"
|
||||||
|
scan-ref: "."
|
||||||
|
format: "table"
|
||||||
|
severity: "CRITICAL,HIGH,MEDIUM"
|
||||||
|
scanners: "vuln,secret,misconfig"
|
||||||
|
|
||||||
|
# ── Summary Gate ─────────────────────────────────────────────────
|
||||||
|
security-gate:
|
||||||
|
name: Security Gate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [dependency-audit, trivy-api, trivy-web, trivy-ai, trivy-fs]
|
||||||
|
if: always()
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check security scan results
|
||||||
|
run: |
|
||||||
|
echo "## Security Scan Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Check each job result
|
||||||
|
FAILED=false
|
||||||
|
|
||||||
|
if [ "${{ needs.dependency-audit.result }}" != "success" ]; then
|
||||||
|
echo "❌ Dependency audit: ${{ needs.dependency-audit.result }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
FAILED=true
|
||||||
|
else
|
||||||
|
echo "✅ Dependency audit: passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ needs.trivy-api.result }}" != "success" ]; then
|
||||||
|
echo "❌ Trivy API scan: ${{ needs.trivy-api.result }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
FAILED=true
|
||||||
|
else
|
||||||
|
echo "✅ Trivy API scan: passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ needs.trivy-web.result }}" != "success" ]; then
|
||||||
|
echo "❌ Trivy Web scan: ${{ needs.trivy-web.result }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
FAILED=true
|
||||||
|
else
|
||||||
|
echo "✅ Trivy Web scan: passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ needs.trivy-ai.result }}" != "success" ]; then
|
||||||
|
echo "❌ Trivy AI scan: ${{ needs.trivy-ai.result }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
FAILED=true
|
||||||
|
else
|
||||||
|
echo "✅ Trivy AI scan: passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ needs.trivy-fs.result }}" != "success" ]; then
|
||||||
|
echo "❌ Trivy filesystem scan: ${{ needs.trivy-fs.result }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
FAILED=true
|
||||||
|
else
|
||||||
|
echo "✅ Trivy filesystem scan: passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$FAILED" = true ]; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ One or more security scans failed. Review the Security tab for details." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 All security scans passed!" >> $GITHUB_STEP_SUMMARY
|
||||||
3
apps/web/i18n/config.ts
Normal file
3
apps/web/i18n/config.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const locales = ['vi', 'en'] as const;
|
||||||
|
export type Locale = (typeof locales)[number];
|
||||||
|
export const defaultLocale: Locale = 'vi';
|
||||||
4
apps/web/i18n/navigation.ts
Normal file
4
apps/web/i18n/navigation.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { createNavigation } from 'next-intl/navigation';
|
||||||
|
import { routing } from './routing';
|
||||||
|
|
||||||
|
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing);
|
||||||
16
apps/web/i18n/request.ts
Normal file
16
apps/web/i18n/request.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { getRequestConfig } from 'next-intl/server';
|
||||||
|
import { routing } from './routing';
|
||||||
|
|
||||||
|
export default getRequestConfig(async ({ requestLocale }) => {
|
||||||
|
let locale = await requestLocale;
|
||||||
|
|
||||||
|
// Ensure a valid locale is used
|
||||||
|
if (!locale || !routing.locales.includes(locale as (typeof routing.locales)[number])) {
|
||||||
|
locale = routing.defaultLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
messages: (await import(`../messages/${locale}.json`)).default,
|
||||||
|
};
|
||||||
|
});
|
||||||
8
apps/web/i18n/routing.ts
Normal file
8
apps/web/i18n/routing.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineRouting } from 'next-intl/routing';
|
||||||
|
import { locales, defaultLocale } from './config';
|
||||||
|
|
||||||
|
export const routing = defineRouting({
|
||||||
|
locales,
|
||||||
|
defaultLocale,
|
||||||
|
localePrefix: 'as-needed',
|
||||||
|
});
|
||||||
110
apps/web/messages/en.json
Normal file
110
apps/web/messages/en.json
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "GoodGo — Vietnam Real Estate Platform",
|
||||||
|
"description": "GoodGo — smart real estate platform in Vietnam. Buy, sell, and rent properties easily with over 10,000+ listings nationwide.",
|
||||||
|
"ogTitle": "GoodGo — Vietnam Real Estate Platform",
|
||||||
|
"ogDescription": "Buy, sell, and rent properties easily with GoodGo — Vietnam's leading smart real estate platform."
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"goodgo": "GoodGo",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"retry": "Retry",
|
||||||
|
"retrying": "Retrying...",
|
||||||
|
"goHome": "Go to homepage",
|
||||||
|
"search": "Search",
|
||||||
|
"login": "Login",
|
||||||
|
"register": "Register",
|
||||||
|
"logout": "Logout",
|
||||||
|
"admin": "Admin",
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"errorCode": "Error code: {code}",
|
||||||
|
"retriedCount": "Retried {count} times",
|
||||||
|
"allRightsReserved": "© 2026 GoodGo. All rights reserved.",
|
||||||
|
"skipToContent": "Skip to main content"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"home": "Home",
|
||||||
|
"search": "Search",
|
||||||
|
"mainNav": "Main navigation",
|
||||||
|
"dashboardNav": "Dashboard",
|
||||||
|
"adminNav": "Administration"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Dashboard",
|
||||||
|
"listings": "Listings",
|
||||||
|
"createListing": "Create listing",
|
||||||
|
"analytics": "Analytics",
|
||||||
|
"aiValuation": "AI Valuation",
|
||||||
|
"profile": "Profile",
|
||||||
|
"subscription": "Subscription",
|
||||||
|
"payments": "Payments",
|
||||||
|
"darkMode": "Switch to dark mode",
|
||||||
|
"lightMode": "Switch to light mode"
|
||||||
|
},
|
||||||
|
"adminNav": {
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"users": "User management",
|
||||||
|
"moderation": "Content moderation",
|
||||||
|
"kyc": "KYC verification",
|
||||||
|
"closeMenu": "Close menu",
|
||||||
|
"openMenu": "Open menu"
|
||||||
|
},
|
||||||
|
"landing": {
|
||||||
|
"heroTitle": "Find your perfect",
|
||||||
|
"heroTitleHighlight": "property",
|
||||||
|
"heroSubtitle": "Smart real estate platform in Vietnam — buy, sell, and rent properties with ease",
|
||||||
|
"searchPlaceholder": "Enter area, project, or keyword...",
|
||||||
|
"transactionTypeLabel": "Type",
|
||||||
|
"featuredTitle": "Featured listings",
|
||||||
|
"featuredSubtitle": "Explore the most popular properties",
|
||||||
|
"viewAll": "View all",
|
||||||
|
"loadError": "Unable to load listings. Please try again.",
|
||||||
|
"noFeatured": "No featured listings yet",
|
||||||
|
"districtsTitle": "Popular areas",
|
||||||
|
"districtsSubtitle": "Search by popular districts",
|
||||||
|
"statsTitle": "GoodGo in numbers",
|
||||||
|
"statsSubtitle": "Vietnam's trusted real estate platform",
|
||||||
|
"ctaTitle": "Have a property to list?",
|
||||||
|
"ctaSubtitle": "List for free today and reach thousands of potential buyers",
|
||||||
|
"registerFree": "Register for free",
|
||||||
|
"searchNow": "Search now"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"listings": "Listings",
|
||||||
|
"users": "Users",
|
||||||
|
"transactions": "Successful transactions",
|
||||||
|
"provinces": "Provinces"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"description": "Smart real estate platform in Vietnam",
|
||||||
|
"propertyTypes": "Property types",
|
||||||
|
"areas": "Areas",
|
||||||
|
"support": "Support"
|
||||||
|
},
|
||||||
|
"propertyTypes": {
|
||||||
|
"APARTMENT": "Apartment",
|
||||||
|
"HOUSE": "House",
|
||||||
|
"VILLA": "Villa",
|
||||||
|
"LAND": "Land",
|
||||||
|
"OFFICE": "Office",
|
||||||
|
"SHOPHOUSE": "Shophouse"
|
||||||
|
},
|
||||||
|
"transactionTypes": {
|
||||||
|
"SALE": "Sale",
|
||||||
|
"RENT": "Rent"
|
||||||
|
},
|
||||||
|
"notFound": {
|
||||||
|
"title": "Page not found",
|
||||||
|
"description": "The page you are looking for does not exist or has been moved."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"title": "An error occurred",
|
||||||
|
"description": "Sorry, something went wrong. Please try again.",
|
||||||
|
"autoRetrying": "Automatically retrying..."
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"label": "Language",
|
||||||
|
"vi": "Tiếng Việt",
|
||||||
|
"en": "English"
|
||||||
|
}
|
||||||
|
}
|
||||||
110
apps/web/messages/vi.json
Normal file
110
apps/web/messages/vi.json
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "GoodGo — Nền tảng Bất động sản Việt Nam",
|
||||||
|
"description": "GoodGo — nền tảng bất động sản thông minh tại Việt Nam. Mua bán, cho thuê nhà đất dễ dàng với hơn 10,000+ tin đăng trên toàn quốc.",
|
||||||
|
"ogTitle": "GoodGo — Nền tảng Bất động sản Việt Nam",
|
||||||
|
"ogDescription": "Mua bán, cho thuê bất động sản dễ dàng với GoodGo — nền tảng thông minh, uy tín hàng đầu Việt Nam."
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"goodgo": "GoodGo",
|
||||||
|
"loading": "Đang tải...",
|
||||||
|
"retry": "Thử lại",
|
||||||
|
"retrying": "Đang thử lại...",
|
||||||
|
"goHome": "Về trang chủ",
|
||||||
|
"search": "Tìm kiếm",
|
||||||
|
"login": "Đăng nhập",
|
||||||
|
"register": "Đăng ký",
|
||||||
|
"logout": "Đăng xuất",
|
||||||
|
"admin": "Admin",
|
||||||
|
"dashboard": "Bảng điều khiển",
|
||||||
|
"errorCode": "Mã lỗi: {code}",
|
||||||
|
"retriedCount": "Đã thử lại {count} lần",
|
||||||
|
"allRightsReserved": "© 2026 GoodGo. Tất cả quyền được bảo lưu.",
|
||||||
|
"skipToContent": "Chuyển đến nội dung chính"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"home": "Trang chủ",
|
||||||
|
"search": "Tìm kiếm",
|
||||||
|
"mainNav": "Điều hướng chính",
|
||||||
|
"dashboardNav": "Bảng điều khiển",
|
||||||
|
"adminNav": "Quản trị"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Bảng điều khiển",
|
||||||
|
"listings": "Tin đăng",
|
||||||
|
"createListing": "Đăng tin",
|
||||||
|
"analytics": "Phân tích",
|
||||||
|
"aiValuation": "Định giá AI",
|
||||||
|
"profile": "Hồ sơ",
|
||||||
|
"subscription": "Gói dịch vụ",
|
||||||
|
"payments": "Thanh toán",
|
||||||
|
"darkMode": "Chuyển sang chế độ tối",
|
||||||
|
"lightMode": "Chuyển sang chế độ sáng"
|
||||||
|
},
|
||||||
|
"adminNav": {
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"users": "Quản lý người dùng",
|
||||||
|
"moderation": "Kiểm duyệt tin",
|
||||||
|
"kyc": "Duyệt KYC",
|
||||||
|
"closeMenu": "Đóng menu",
|
||||||
|
"openMenu": "Mở menu"
|
||||||
|
},
|
||||||
|
"landing": {
|
||||||
|
"heroTitle": "Tìm kiếm bất động sản",
|
||||||
|
"heroTitleHighlight": "hoàn hảo",
|
||||||
|
"heroSubtitle": "Nền tảng bất động sản thông minh tại Việt Nam — mua bán, cho thuê nhà đất dễ dàng",
|
||||||
|
"searchPlaceholder": "Nhập khu vực, dự án, hoặc từ khóa...",
|
||||||
|
"transactionTypeLabel": "Loại GD",
|
||||||
|
"featuredTitle": "Tin đăng nổi bật",
|
||||||
|
"featuredSubtitle": "Khám phá các bất động sản được quan tâm nhất",
|
||||||
|
"viewAll": "Xem tất cả",
|
||||||
|
"loadError": "Không thể tải tin đăng. Vui lòng thử lại.",
|
||||||
|
"noFeatured": "Chưa có tin đăng nổi bật",
|
||||||
|
"districtsTitle": "Khu vực nổi bật",
|
||||||
|
"districtsSubtitle": "Tìm kiếm theo quận huyện phổ biến",
|
||||||
|
"statsTitle": "GoodGo trong số liệu",
|
||||||
|
"statsSubtitle": "Nền tảng bất động sản đáng tin cậy tại Việt Nam",
|
||||||
|
"ctaTitle": "Bạn có bất động sản muốn đăng?",
|
||||||
|
"ctaSubtitle": "Đăng tin miễn phí ngay hôm nay, tiếp cận hàng ngàn người mua tiềm năng",
|
||||||
|
"registerFree": "Đăng ký miễn phí",
|
||||||
|
"searchNow": "Tìm kiếm ngay"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"listings": "Tin đăng",
|
||||||
|
"users": "Người dùng",
|
||||||
|
"transactions": "Giao dịch thành công",
|
||||||
|
"provinces": "Tỉnh thành"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"description": "Nền tảng bất động sản thông minh tại Việt Nam",
|
||||||
|
"propertyTypes": "Loại BĐS",
|
||||||
|
"areas": "Khu vực",
|
||||||
|
"support": "Hỗ trợ"
|
||||||
|
},
|
||||||
|
"propertyTypes": {
|
||||||
|
"APARTMENT": "Căn hộ",
|
||||||
|
"HOUSE": "Nhà riêng",
|
||||||
|
"VILLA": "Biệt thự",
|
||||||
|
"LAND": "Đất nền",
|
||||||
|
"OFFICE": "Văn phòng",
|
||||||
|
"SHOPHOUSE": "Shophouse"
|
||||||
|
},
|
||||||
|
"transactionTypes": {
|
||||||
|
"SALE": "Bán",
|
||||||
|
"RENT": "Cho thuê"
|
||||||
|
},
|
||||||
|
"notFound": {
|
||||||
|
"title": "Không tìm thấy trang",
|
||||||
|
"description": "Trang bạn đang tìm không tồn tại hoặc đã được di chuyển."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"title": "Đã xảy ra lỗi",
|
||||||
|
"description": "Rất tiếc, đã có lỗi xảy ra. Vui lòng thử lại.",
|
||||||
|
"autoRetrying": "Đang tự động thử lại..."
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"label": "Ngôn ngữ",
|
||||||
|
"vi": "Tiếng Việt",
|
||||||
|
"en": "English"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user