랜딩 페이지: D3RO 브랜드 프로모션 사이트 + GitHub Pages 배포
- Vite + React 19 + Tailwind CSS - 4가지 디자인 레퍼런스 통합 (CRT 스크린, 크로스헤어, 노이즈, 인스트루먼트) - 섹션 9개: Hero, Features, HowItWorks, Privacy, Pricing, FAQ, CTA, Header, Footer - 재사용 컴포넌트 8개 (Badge, Container, InstrumentCard, GlowButton 등) - i18n 10개 국어 (en/ko/ja/zh/es/fr/de/pt/ru/vi) - GitHub Pages 자동 배포 워크플로우
This commit is contained in:
parent
eb83682269
commit
5892125740
45 changed files with 6225 additions and 0 deletions
227
site/src/i18n/index.ts
Normal file
227
site/src/i18n/index.ts
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react'
|
||||
import { createElement } from 'react'
|
||||
import { en } from './locales/en'
|
||||
import { ko } from './locales/ko'
|
||||
import { ja } from './locales/ja'
|
||||
import { zh } from './locales/zh'
|
||||
import { es } from './locales/es'
|
||||
import { fr } from './locales/fr'
|
||||
import { de } from './locales/de'
|
||||
import { pt } from './locales/pt'
|
||||
import { ru } from './locales/ru'
|
||||
import { vi } from './locales/vi'
|
||||
|
||||
// ── Types ──────────────────────────────────────────────
|
||||
export type Locale = 'en' | 'ko' | 'ja' | 'zh' | 'es' | 'fr' | 'de' | 'pt' | 'ru' | 'vi'
|
||||
|
||||
export interface FeatureItem {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface PipelineStep {
|
||||
label: string
|
||||
title: string
|
||||
description: string
|
||||
detail: string
|
||||
}
|
||||
|
||||
export interface PrivacyMetricItem {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface FaqItem {
|
||||
q: string
|
||||
a: string
|
||||
}
|
||||
|
||||
export interface PricingFeatureRow {
|
||||
feature: string
|
||||
free: string | boolean
|
||||
pro: string | boolean
|
||||
proPlus: string | boolean
|
||||
}
|
||||
|
||||
export interface Translations {
|
||||
nav: {
|
||||
features: string
|
||||
pipeline: string
|
||||
privacy: string
|
||||
pricing: string
|
||||
faq: string
|
||||
download: string
|
||||
}
|
||||
hero: {
|
||||
badge: string
|
||||
title1: string
|
||||
title2: string
|
||||
title3: string
|
||||
subtitle: string
|
||||
downloadBtn: string
|
||||
viewFeatures: string
|
||||
dataProcessing: string
|
||||
dataCloudTraffic: string
|
||||
dataLatency: string
|
||||
dataPrivacy: string
|
||||
systemStatus: string
|
||||
protocol: string
|
||||
}
|
||||
features: {
|
||||
index: string
|
||||
title: string
|
||||
subtitle: string
|
||||
items: [FeatureItem, FeatureItem, FeatureItem, FeatureItem, FeatureItem, FeatureItem, FeatureItem, FeatureItem, FeatureItem]
|
||||
}
|
||||
pipeline: {
|
||||
index: string
|
||||
title: string
|
||||
subtitle: string
|
||||
steps: [PipelineStep, PipelineStep, PipelineStep, PipelineStep]
|
||||
panelTitle: string
|
||||
panelActive: string
|
||||
panelLatency: string
|
||||
sttReady: string
|
||||
llmConnected: string
|
||||
transcription: string
|
||||
aiPolish: string
|
||||
sampleInput: string
|
||||
sampleOutput: string
|
||||
}
|
||||
privacy: {
|
||||
index: string
|
||||
title: string
|
||||
subtitle: string
|
||||
monitorTitle: string
|
||||
allClear: string
|
||||
metrics: [PrivacyMetricItem, PrivacyMetricItem, PrivacyMetricItem, PrivacyMetricItem, PrivacyMetricItem, PrivacyMetricItem]
|
||||
guarantees: [string, string, string, string, string, string]
|
||||
}
|
||||
pricing: {
|
||||
index: string
|
||||
title: string
|
||||
subtitle: string
|
||||
featureLabel: string
|
||||
free: string
|
||||
pro: string
|
||||
proPlus: string
|
||||
forever: string
|
||||
oneTime: string
|
||||
popular: string
|
||||
downloadFree: string
|
||||
getPro: string
|
||||
getProPlus: string
|
||||
taxNote: string
|
||||
rows: [PricingFeatureRow, PricingFeatureRow, PricingFeatureRow, PricingFeatureRow, PricingFeatureRow, PricingFeatureRow, PricingFeatureRow, PricingFeatureRow, PricingFeatureRow, PricingFeatureRow, PricingFeatureRow, PricingFeatureRow, PricingFeatureRow, PricingFeatureRow, PricingFeatureRow]
|
||||
}
|
||||
faq: {
|
||||
index: string
|
||||
title: string
|
||||
items: [FaqItem, FaqItem, FaqItem, FaqItem, FaqItem, FaqItem]
|
||||
}
|
||||
cta: {
|
||||
title1: string
|
||||
title2: string
|
||||
subtitle1: string
|
||||
subtitle2: string
|
||||
downloadBtn: string
|
||||
systemReq: string
|
||||
}
|
||||
footer: {
|
||||
description: string
|
||||
copyright: string
|
||||
builtWith: string
|
||||
}
|
||||
}
|
||||
|
||||
// ── Locale metadata ────────────────────────────────────
|
||||
export interface LocaleMeta {
|
||||
code: Locale
|
||||
label: string
|
||||
nativeName: string
|
||||
}
|
||||
|
||||
export const LOCALES: LocaleMeta[] = [
|
||||
{ code: 'en', label: 'EN', nativeName: 'English' },
|
||||
{ code: 'ko', label: 'KO', nativeName: '한국어' },
|
||||
{ code: 'ja', label: 'JA', nativeName: '日本語' },
|
||||
{ code: 'zh', label: 'ZH', nativeName: '中文' },
|
||||
{ code: 'es', label: 'ES', nativeName: 'Espanol' },
|
||||
{ code: 'fr', label: 'FR', nativeName: 'Francais' },
|
||||
{ code: 'de', label: 'DE', nativeName: 'Deutsch' },
|
||||
{ code: 'pt', label: 'PT', nativeName: 'Portugues' },
|
||||
{ code: 'ru', label: 'RU', nativeName: 'Русский' },
|
||||
{ code: 'vi', label: 'VI', nativeName: 'Tieng Viet' },
|
||||
]
|
||||
|
||||
// ── Translations map ───────────────────────────────────
|
||||
const translations: Record<Locale, Translations> = {
|
||||
en, ko, ja, zh, es, fr, de, pt, ru, vi,
|
||||
}
|
||||
|
||||
// ── Storage ────────────────────────────────────────────
|
||||
const STORAGE_KEY = 'd3ro-locale'
|
||||
|
||||
function detectLocale(): Locale {
|
||||
// 1. Check localStorage
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored && stored in translations) return stored as Locale
|
||||
} catch { /* noop */ }
|
||||
|
||||
// 2. Check browser language
|
||||
const browserLang = navigator.language.toLowerCase()
|
||||
const exact = LOCALES.find((l) => browserLang === l.code || browserLang.startsWith(l.code + '-'))
|
||||
if (exact) return exact.code
|
||||
|
||||
// 3. Fallback
|
||||
return 'en'
|
||||
}
|
||||
|
||||
function persistLocale(locale: Locale): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, locale)
|
||||
} catch { /* noop */ }
|
||||
}
|
||||
|
||||
// ── Context ────────────────────────────────────────────
|
||||
interface I18nContextValue {
|
||||
locale: Locale
|
||||
t: Translations
|
||||
setLocale: (locale: Locale) => void
|
||||
}
|
||||
|
||||
const I18nContext = createContext<I18nContextValue | null>(null)
|
||||
|
||||
export function useI18n(): I18nContextValue {
|
||||
const ctx = useContext(I18nContext)
|
||||
if (!ctx) throw new Error('useI18n must be used within I18nProvider')
|
||||
return ctx
|
||||
}
|
||||
|
||||
// ── Provider ───────────────────────────────────────────
|
||||
interface I18nProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function I18nProvider({ children }: I18nProviderProps) {
|
||||
const [locale, setLocaleState] = useState<Locale>(detectLocale)
|
||||
|
||||
const setLocale = useCallback((newLocale: Locale) => {
|
||||
setLocaleState(newLocale)
|
||||
persistLocale(newLocale)
|
||||
document.documentElement.lang = newLocale
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale
|
||||
}, [locale])
|
||||
|
||||
const value: I18nContextValue = {
|
||||
locale,
|
||||
t: translations[locale],
|
||||
setLocale,
|
||||
}
|
||||
|
||||
return createElement(I18nContext.Provider, { value }, children)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue