diff --git a/app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/ProgramSessionClient.tsx b/app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/ProgramSessionClient.tsx
index 81c773b5..db0224a2 100644
--- a/app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/ProgramSessionClient.tsx
+++ b/app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/ProgramSessionClient.tsx
@@ -74,12 +74,14 @@ export function ProgramSessionClient({ program, week, session, isAuthenticated,
namePt: ex.exercise.namePt || null,
nameRu: ex.exercise.nameRu || null,
nameZhCn: ex.exercise.nameZhCn || null,
+ nameKo: ex.exercise.nameKo || null,
description: ex.exercise.description || null,
descriptionEn: ex.exercise.descriptionEn || null,
descriptionEs: ex.exercise.descriptionEs || null,
descriptionPt: ex.exercise.descriptionPt || null,
descriptionRu: ex.exercise.descriptionRu || null,
descriptionZhCn: ex.exercise.descriptionZhCn || null,
+ descriptionKo: ex.exercise.descriptionKo || null,
fullVideoUrl: ex.exercise.fullVideoUrl || null,
fullVideoImageUrl: ex.exercise.fullVideoImageUrl || null,
introduction: null,
diff --git a/app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/page.tsx b/app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/page.tsx
index 340aff3e..64272e40 100644
--- a/app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/page.tsx
+++ b/app/[locale]/(app)/programs/[slug]/session/[sessionSlug]/page.tsx
@@ -66,6 +66,7 @@ export async function generateMetadata({ params }: SessionDetailPageProps): Prom
"pt-PT": `https://www.workout.cool/pt/programs/${slug}/session/${sessionSlug}`,
"ru-RU": `https://www.workout.cool/ru/programs/${slug}/session/${sessionSlug}`,
"zh-CN": `https://www.workout.cool/zh-CN/programs/${slug}/session/${sessionSlug}`,
+ "ko-KR": `https://www.workout.cool/ko/programs/${slug}/session/${sessionSlug}`,
"x-default": `https://www.workout.cool/programs/${slug}/session/${sessionSlug}`,
},
},
diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx
index c5d31d03..bcb974d0 100644
--- a/app/[locale]/layout.tsx
+++ b/app/[locale]/layout.tsx
@@ -66,7 +66,9 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
? "ru_RU"
: locale === "zh-CN"
? "zh_CN"
- : "fr_FR",
+ : locale === "ko"
+ ? "ko_KR"
+ : "fr_FR",
alternateLocale: [
"fr_FR",
"fr_CA",
@@ -88,6 +90,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
"zh_CN",
"zh_TW",
"zh_HK",
+ "ko_KR",
].filter(
(alt) =>
alt !==
@@ -101,7 +104,9 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
? "ru_RU"
: locale === "zh-CN"
? "zh_CN"
- : "fr_FR"),
+ : locale === "ko"
+ ? "ko_KR"
+ : "fr_FR"),
),
images: [
{
@@ -167,6 +172,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
"pt-PT": "https://www.workout.cool/pt",
"ru-RU": "https://www.workout.cool/ru",
"zh-CN": "https://www.workout.cool/zh-CN",
+ "ko-KR": "https://www.workout.cool/ko",
"x-default": "https://www.workout.cool",
},
},
@@ -266,6 +272,7 @@ export default async function RootLayout({ params, children }: RootLayoutProps)
+
{/* Theme color for PWA */}
diff --git a/app/sitemap.ts b/app/sitemap.ts
index 4c914171..6f71b5f5 100644
--- a/app/sitemap.ts
+++ b/app/sitemap.ts
@@ -7,7 +7,7 @@ export default async function sitemap(): Promise {
const currentDate = new Date().toISOString();
// Static routes with locale support
- const locales = ["fr", "en", "es", "pt", "ru", "zh-CN"];
+ const locales = ["fr", "en", "es", "pt", "ru", "zh-CN", "ko"];
const staticRoutes = [
// Home page (root)
@@ -157,6 +157,7 @@ export default async function sitemap(): Promise {
pt: program.slugPt,
ru: program.slugRu,
"zh-CN": program.slugZhCn,
+ ko: program.slugKo,
};
Object.entries(programSlugs).forEach(([locale, slug]) => {
@@ -179,6 +180,7 @@ export default async function sitemap(): Promise {
pt: session.slugPt,
ru: session.slugRu,
"zh-CN": session.slugZhCn,
+ ko: session.slugKo,
};
const sessionSlug = sessionSlugs[locale as keyof typeof sessionSlugs];
diff --git a/content/about/ko.mdx b/content/about/ko.mdx
new file mode 100644
index 00000000..068916b3
--- /dev/null
+++ b/content/about/ko.mdx
@@ -0,0 +1,45 @@
+import Link from "next/link";
+import { WorkoutLol } from "@/components/icons/WorkoutLol";
+
+# Workout.cool 소개
+
+## 왜 Workout.cool인가요?
+
+기존 프로젝트인 가 방치된 후, 신뢰할 수 있고 현대적이며 활발하게 관리되는 운동 플랫폼을 제공하고자 하는
+열망에서 Workout.cool이 탄생했습니다.
+
+## 탄생 스토리
+
+Workout.cool은 커뮤니티 주도형 여정의 결과물입니다.
+
+저는 프로젝트의 **첫 번째 오픈소스 기여자**였습니다.
+
+이 말은 제가 이 프로젝트가 *탄생*하고, *성장*하다가, **매각**된 후 결국 새로운 소유주에 의해 **방치**되는 것을 지켜봤다는 의미입니다.
+
+많은 사용자들과 마찬가지로, 저는 제가 많은 기여를 했던 도구가 사라지고, 기능 요청들은 답변 없이 쌓여가는 것을 보며 **깊은 좌절감**과
+*버려졌다는 느낌*을 받았습니다.
+
+---
+
+_수개월 동안_, 저는 새로운 소유주에게 연락을 시도했지만, 여러 번의 시도(_약 15번_)에도 불구하고 **단 한 번의 답장도 받지 못했습니다**.
+
+이러한 **침묵**과 **커뮤니티의 절망감**에 직면하여, 저는 **직접 나서기로 결심했습니다**:
+
+> 이 모든 노력이 사라지게 두는 대신, **저는 모두를 위해 훨씬 더 야심 차고, 현대적이며, 열린 프로젝트를 다시 시작했습니다.**
+
+이 프로젝트는 이윤이 아닌, **열정**과 오픈소스 피트니스 커뮤니티에 봉사하고자 하는 마음으로 운영됩니다.
+
+**누군가는 이 커뮤니티를 구해야 했습니다—_제가 그 '누군가'가 되기로 했습니다!_**
+
+## 오픈소스 & 커뮤니티
+
+Workout.cool은 투명성, 모듈성, 확장성을 보장하는 오픈소스입니다. 코드, 문서, 아이디어 등 누구든지 기여를 환영합니다!
+
+- [GitHub에서 프로젝트 보기](https://github.com/Snouzy/workout-cool)
+- [커피 한 잔 으로 후원하기](https://ko-fi.com/workoutcool)
+
+## 미션에 동참하세요!
+
+기여하고 싶거나, 기능 제안 또는 단순히 프로젝트를 지원하고 싶으신가요? 저희에게 연락하거나 GitHub에 이슈를 열어주세요!
+
+**[hello@workout.cool](mailto:hello@workout.cool)**
diff --git a/content/privacy-policy/ko.mdx b/content/privacy-policy/ko.mdx
new file mode 100644
index 00000000..44912795
--- /dev/null
+++ b/content/privacy-policy/ko.mdx
@@ -0,0 +1,91 @@
+## WorkoutCool 개인정보 처리방침
+
+### 1. 서문
+
+WorkoutCool은 사용자 여러분의 개인정보를 매우 중요하게 생각합니다.
+
+본 개인정보 처리방침은 당사가 사용자의 개인 데이터를 수집, 사용 및 보호하는 방법을 설명합니다.
+
+당사 서비스를 이용함으로써 귀하는 아래 명시된 관행에 동의하게 됩니다.
+
+---
+
+### 2. 수집하는 정보
+
+- **개인정보**: 회원가입 시 당사는 귀하의 이름, 이메일 주소 및 WorkoutCool에서의 활동과 관련된 기타 세부 정보를 수집합니다.
+- **사용 데이터**: 당사는 클릭, 조회수, 링크 성과를 포함하여 WorkoutCool 페이지와의 상호작용을 추적합니다.
+- **쿠키**: 당사는 사용자 경험을 향상하고 사용량을 모니터링하기 위해 쿠키를 사용합니다.
+
+---
+
+### 3. 정보 사용 방법
+
+- **서비스 제공**: 서비스 제공, 유지 및 개선을 위함입니다.
+- **커뮤니케이션**: 업데이트, 알림 또는 마케팅 콘텐츠를 보내기 위함입니다 (귀하가 동의한 경우).
+- **분석**: 당사 서비스가 어떻게 사용되는지 이해하고 지속적으로 개선하기 위함입니다.
+
+---
+
+### 4. 제3자와의 데이터 공유
+
+당사는 마케팅, 분석 또는 제품 개선을 목적으로 신뢰할 수 있는 파트너와 집계되거나 익명화된 데이터를 공유할 수 있습니다.
+
+귀하의 명시적인 동의 없이 개인 데이터는 판매되거나 공유되지 않습니다.
+
+---
+
+### 5. 데이터 보존
+
+귀하의 데이터는 계정이 활성화되어 있거나 서비스 제공에 필요한 기간 동안 보존됩니다.
+
+일부 데이터는 법적, 행정적 또는 보안 목적으로 보관될 수 있습니다.
+
+---
+
+### 6. 데이터 보호
+
+당사는 귀하의 데이터에 대한 무단 접근, 변경 또는 삭제를 방지하기 위해 업계 표준 보안 조치를 구현합니다.
+
+---
+
+### 7. 귀하의 권리
+
+귀하는 다음 권리를 가집니다:
+
+- **접근**: 개인 데이터 사본을 요청할 수 있습니다.
+- **정정**: 부정확하거나 불완전한 데이터의 정정을 요청할 수 있습니다.
+- **삭제**: 법률에 의해 보존이 요구되지 않는 한, 귀하의 데이터 삭제를 요청할 수 있습니다.
+
+---
+
+### 8. 쿠키
+
+WorkoutCool은 쿠키를 사용하여 다음을 수행합니다:
+
+- 탐색 기능 향상,
+- 성능 추적,
+- 표시되는 콘텐츠 개인화.
+
+브라우저 설정에서 쿠키를 비활성화할 수 있습니다.
+
+---
+
+### 9. 광고 및 규정 준수
+
+당사는 Facebook 및 Google과 같은 플랫폼의 광고 정책을 준수합니다.
+
+당사는 귀하의 데이터를 해당 지침이나 적용 가능한 규정을 위반하는 방식으로 사용하지 않습니다.
+
+---
+
+### 10. 본 정책의 변경
+
+본 개인정보 처리방침은 수시로 업데이트될 수 있습니다. 정기적으로 검토하시기를 권장합니다.
+
+당사 서비스를 계속 사용하는 것은 모든 변경 사항에 대한 동의를 의미합니다.
+
+---
+
+### 11. 문의
+
+본 정책에 대한 질문이나 우려 사항이 있으시면 언제든지 다음으로 문의하십시오: **[hello@WorkoutCool.io](mailto:hello@WorkoutCool.io)**
diff --git a/content/sales-terms/ko.mdx b/content/sales-terms/ko.mdx
new file mode 100644
index 00000000..a4e5c03b
--- /dev/null
+++ b/content/sales-terms/ko.mdx
@@ -0,0 +1,127 @@
+# 일반 판매 약관 – WorkoutCool
+
+## 제1조: 목적
+
+본 약관은 WorkoutCool 플랫폼(SaaS 모델)을 통해 제공되는 서비스에 적용됩니다.
+
+요금제를 구독하거나 WorkoutCool 애플리케이션을 사용하는 고객은 이용 약관 및 개인정보 처리방침과 함께 본 판매 약관에 전적으로 무조건적으로 동의합니다.
+
+---
+
+## 제2조: 정의
+
+- **구독자**: 유료 요금제를 구독한 모든 개인 또는 법인.
+- **구독**: 애플리케이션의 특정 기능에 대한 접근 권한을 부여하는 유료 서비스.
+- **애플리케이션**: WorkoutCool 웹 애플리케이션 (및 사용 가능한 경우 모바일 앱).
+- **고객**: 개인적 또는 전문적 용도로 플랫폼을 사용하는 이용자.
+- **회사**: Mathias BRADICEANU가 운영하는 WorkoutCool을 지칭합니다.
+
+---
+
+## 제3조: 주문 및 활성화
+
+구독은 WorkoutCool.io를 통해 온라인으로 구매됩니다.
+
+접근 권한은 결제 성공 확인 후 활성화됩니다.
+
+회사는 특히 사기, 남용 또는 본 약관 위반이 의심되는 경우 모든 주문을 거부하거나 취소할 권리를 가집니다.
+
+---
+
+## 제4조: 가격 및 결제
+
+구독료는 선택한 기간(월간, 연간 등)에 대해 선불로 청구됩니다.
+
+결제 실패 시 통지 없이 즉시 접근이 중단됩니다.
+
+가격은 모든 해당 세금이 포함된 유로화로 표시됩니다.
+
+가격은 언제든지 변경될 수 있지만, 향후 갱신에만 적용됩니다.
+
+본 약관 위반으로 인한 중단 시 환불은 불가합니다.
+
+---
+
+## 제5조: 기간, 갱신, 체험 기간 및 철회
+
+구독은 고정 기간이며 사전에 취소하지 않는 한 자동 갱신됩니다.
+
+고객은 갱신일 전에 개인 계정을 통해 언제든지 취소할 수 있습니다.
+
+무료 14일 체험 기간은 사용자당 1회 제공됩니다.
+
+이 제한을 우회하려는 시도(여러 계정, 허위 신원)는 즉시 중단 및 법적 조치로 이어질 수 있습니다.
+
+철회권은 프랑스 소비자법 제L221-28조에 따라 적용되지만, **체험 기간 동안 서비스를 사용한 경우에는 적용되지 않습니다.**
+
+모든 철회 요청은 [support@WorkoutCool.io](mailto:support@WorkoutCool.io)로 이메일을 보내거나 회사의 등록 주소로 등기우편을 통해 보내야 합니다.
+
+---
+
+## 제6조: 고객 의무
+
+고객은 다음에 동의합니다:
+
+- 자신의 계정 접근 권한을 공유, 양도 또는 판매하지 않을 것.
+- 체험 기간을 사기적으로 이용하지 않을 것.
+- WorkoutCool의 지적 재산권을 존중할 것.
+- 플랫폼의 무결성 또는 보안을 해치는 어떠한 행위도 피할 것.
+- 정확하고 최신 청구 및 연락처 정보를 제공할 것.
+- 구독료를 제때 결제할 것.
+
+어떠한 위반도 사전 통지나 보상 없이 계정 중단 또는 삭제로 이어질 수 있습니다.
+
+---
+
+## 제7조: 책임
+
+WorkoutCool은 다음에 대해 책임을 지지 않습니다:
+
+- 인터넷 관련 문제 또는 고객 측 기술 문제.
+- 유지보수로 인한 일시적인 서비스 중단.
+- 제3자에 의한 서비스의 불법적이거나 부적절한 사용.
+- 고객이 자신의 콘텐츠를 백업하지 않아 발생한 데이터 손실.
+
+서비스가 고객의 특정 요구에 적합하다는 어떠한 보증도 제공되지 않습니다.
+
+회사는 보상 의무 없이 모든 서비스 기능을 중단, 수정 또는 제거할 수 있습니다.
+
+---
+
+## 제8조: 면책
+
+고객은 오용, 불법 사용 또는 본 약관 위반으로 인해 발생하는 모든 청구, 손실 또는 책임(법률 및 행정 수수료 포함)으로부터 WorkoutCool을 면책하고 방어하는 데 동의합니다.
+
+---
+
+## 제9조: 약관 변경
+
+WorkoutCool은 본 판매 약관을 언제든지 업데이트할 권리를 가집니다.
+
+적용되는 버전은 고객의 주문 또는 갱신 시점에 웹사이트에서 제공되는 버전입니다.
+
+고객은 가장 최신 버전을 정기적으로 확인하는 것이 좋습니다.
+
+---
+
+## 제10조: 불가항력
+
+WorkoutCool은 자연재해, 팬데믹, 사이버 공격, 정전, 화재, 전쟁, 파업 또는 기타 예측 불가능한 사건과 같이 합리적인 통제를 벗어나는 사건으로 인해 발생하는 서비스 실패 또는 지연에 대해 책임을 지지 않습니다.
+
+---
+
+## 제11조: 증명 및 보관
+
+WorkoutCool 시스템에 저장된 디지털 기록은 거래 및 통신의 유효한 증거를 구성합니다.
+
+청구서는 사용자 계정에서 언제든지 확인할 수 있습니다.
+
+---
+
+## 제12조: 연락처 – 불만 사항
+
+질문, 불만 사항 또는 문제가 있는 경우 고객은 다음을 통해 WorkoutCool에 문의할 수 있습니다:
+
+- 이메일: [support@WorkoutCool.io](mailto:support@WorkoutCool.io)
+- 우편: Mathias BRADICEANU, Strada Fagului 40F, 077010 Afumați, Romania
+- 앱 내 메시징 시스템
\ No newline at end of file
diff --git a/content/terms/ko.mdx b/content/terms/ko.mdx
new file mode 100644
index 00000000..fca99ce4
--- /dev/null
+++ b/content/terms/ko.mdx
@@ -0,0 +1,157 @@
+# 이용 약관 – WorkoutCool
+
+본 이용 약관("약관")은 WorkoutCool 플랫폼에서 제공하는 서비스의 접근 및 사용 조건을 정의하고, WorkoutCool과 사용자 간의 권리 및 의무를 규정합니다.
+
+_최종 업데이트: 2025년 5월 3일_
+
+---
+
+## 제1조: 법적 고지
+
+WorkoutCool.io 웹사이트는 **Mathias BRADICEANU**에 의해 게시되었습니다.
+
+웹사이트는 **Vercel Inc.**, 440 N Barranca Ave #4133, Covina, CA 91723, USA에서 호스팅됩니다.
+
+---
+
+## 제2조: 플랫폼 접근
+
+WorkoutCool은 사용자가 다음을 수행할 수 있도록 합니다
+
+* 개인화된 바이오 링크 페이지 생성 및 관리
+* 공개 프로필에 링크, 미디어 및 모듈 추가
+* 참여 통계 모니터링 (클릭, 조회수 등)
+* 외관 및 콘텐츠 레이아웃 사용자 지정
+
+일부 기능은 유료 구독을 통해서만 또는 무료 체험 기간 동안에만 사용할 수 있습니다.
+
+플랫폼 접근은 "있는 그대로" 제공되며, 결과에 대한 어떠한 의무도 구성하지 않습니다.
+
+---
+
+## 제3조: 데이터 수집
+
+WorkoutCool은 사용자가 입력하거나 서비스를 사용하는 동안 자동으로 생성되는 개인 데이터를 수집하고 저장합니다.
+
+여기에는 이름, 이메일 주소, 링크, 페이지 콘텐츠 및 사용 통계가 포함됩니다.
+
+사용자는 법적 의무에 따라 자신의 개인 데이터에 대한 접근, 수정 또는 삭제를 요청할 수 있습니다.
+
+---
+
+## 제4조: 지적 재산권
+
+WorkoutCool 플랫폼의 모든 요소(텍스트, 이미지, 코드, 로고, 인터페이스, 웹 구성 요소 등)는 지적 재산권법에 의해 보호되며 WorkoutCool 또는 그 파트너의 독점 재산으로 유지됩니다.
+
+사전 서면 동의 없는 복제, 배포 또는 상업적 사용은 엄격히 금지됩니다.
+
+사용자 계정 및 호스팅된 콘텐츠는 양도할 수 없으며 WorkoutCool의 승인을 받아야 합니다.
+
+---
+
+## 제5조: 사용자 책임
+
+사용자는 다음을 동의합니다.
+
+* 정확하고 합법적인 정보를 제공할 것.
+* 불법적이거나, 불쾌하거나, 명예를 훼손하거나, 오해의 소지가 있는 콘텐츠를 공유하지 않을 것.
+* 자신의 계정과 자격 증명을 안전하게 유지할 것.
+* 적용 가능한 법률 및 커뮤니티 표준을 준수할 것.
+* 사기, 무단 상업적 또는 경쟁적 목적으로 플랫폼을 사용하지 않을 것.
+
+위반 시 WorkoutCool은 통지 또는 환불 없이 위반 계정을 중단하거나 삭제할 수 있습니다.
+
+---
+
+## 제6조: 가용성 및 보증 부인
+
+WorkoutCool은 안정적이고 안전한 서비스를 제공하기 위해 모든 노력을 기울입니다.
+
+그러나 플랫폼은 가용성, 성능, 호환성 또는 오류 없는 작동을 포함하되 이에 국한되지 않는 **명시적 또는 묵시적 보증 없이** 제공됩니다.
+
+기술 지원은 특정 제안에 명시되지 않는 한 계약적으로 보장되지 않습니다.
+
+사용자는 자신의 콘텐츠 및 데이터를 백업할 전적인 책임이 있습니다.
+
+---
+
+## 제7조: 책임의 제한
+
+WorkoutCool은 다음으로 인해 발생하는 직접적 또는 간접적 손해(물질적 또는 비물질적 손실 포함)에 대해 책임을 지지 않습니다.
+
+* 서비스 중단
+* 데이터 손실 또는 손상
+* 오류, 지연 또는 기술적 결함
+* 사용자 또는 제3자에 의한 플랫폼의 부적절하거나 불법적인 사용
+
+WorkoutCool이 책임이 있는 것으로 판명될 경우, 그 책임은 사용자가 지불한 마지막 구독료 금액으로 명시적으로 제한됩니다.
+
+---
+
+## 제8조: 계정 중단 또는 해지
+
+WorkoutCool은 다음의 경우 모든 계정을 중단하거나 해지할 권리를 가집니다.
+
+* 본 약관 위반의 경우
+* 사기적이거나 의심스러운 행동의 경우
+* 불법적이거나 부적절한 콘텐츠의 경우
+* 서비스의 과도하거나 남용적인 사용의 경우
+
+계약 위반으로 인한 중단 또는 해지 시 환불은 불가합니다.
+
+---
+
+## 제9조: 서비스 수정
+
+WorkoutCool은 언제든지 서비스, 기능, 가격 또는 접근 조건을 수정할 수 있습니다.
+
+사용자는 주요 변경 사항에 대해 합리적인 기간 내에 통보받을 것입니다.
+
+플랫폼을 계속 사용하는 것은 그러한 변경 사항에 대한 동의를 의미합니다.
+
+---
+
+## 제10조: 외부 링크
+
+사용자가 생성한 페이지는 제3자 웹사이트로의 링크를 포함할 수 있습니다.
+
+WorkoutCool은 외부 웹사이트의 콘텐츠, 보안 또는 성능에 대해 책임을 지지 않습니다.
+
+---
+
+## 제11조: 가역성
+
+계정 삭제 또는 플랫폼 종료 시, 사용자는 사전에 자신의 데이터를 내보내고 보호할 책임이 있습니다.
+
+WorkoutCool은 전용 제안에 명시적으로 언급되지 않는 한, 제3자 서비스로의 자동 데이터 이동성을 보장하지 않습니다.
+
+---
+
+## 제12조: 면책
+
+사용자는 다음으로 인해 발생하는 모든 청구, 책임, 손실, 손해 또는 비용(법률 수수료 포함)으로부터 WorkoutCool을 방어, 면책 및 무해하게 유지하는 데 동의합니다.
+
+* 본 약관 위반
+* 플랫폼을 통해 게시된 콘텐츠
+* 승인되지 않은 경우라도 자신의 계정을 통해 수행된 활동
+
+---
+
+## 제13조: 연락처
+
+본 약관에 대한 질문이 있으시면 다음으로 문의할 수 있습니다.
+**[support@WorkoutCool.io](mailto:support@WorkoutCool.io)**
+또는 제1조에 명시된 게시자의 주소로 우편을 통해 문의할 수 있습니다.
+
+---
+
+## 제14조: 준거법 및 관할권
+
+본 약관은 프랑스 법률의 적용을 받습니다.
+분쟁 발생 시, 소비자 보호 규정에서 달리 요구하지 않는 한, **프랑스 뮐루즈** 법원이 독점적인 관할권을 가집니다.
+
+---
+
+## 제15조: 불가항력
+
+WorkoutCool은 자연재해, 화재, 홍수, 폭동, 전쟁, 팬데믹, 파업, 사이버 공격, 인프라 장애 또는 그 통제를 벗어나는 기타 예측 불가능한 사건을 포함하되 이에 국한되지 않는 불가항력의 경우, 의무를 이행하지 못한 것에 대해 책임을 지지 않습니다.
\ No newline at end of file
diff --git a/locales/client.ts b/locales/client.ts
index cacdf8f3..78a3d6d0 100644
--- a/locales/client.ts
+++ b/locales/client.ts
@@ -3,7 +3,7 @@
import { createI18nClient } from "next-international/client";
// NOTE: Also update middleware.ts to support locale
-export const languages = ["en", "fr", "es", "zh-CN", "ru", "pt"];
+export const languages = ["en", "fr", "es", "zh-CN", "ru", "pt", "ko"];
export const { useI18n, useScopedI18n, I18nProviderClient, useChangeLocale, defineLocale, useCurrentLocale } = createI18nClient(
{
@@ -31,6 +31,10 @@ export const { useI18n, useScopedI18n, I18nProviderClient, useChangeLocale, defi
await new Promise((resolve) => setTimeout(resolve, 100));
return import("./pt");
},
+ ko: async () => {
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ return import("./ko");
+ },
},
{
// Uncomment to set base path
@@ -42,4 +46,4 @@ export const { useI18n, useScopedI18n, I18nProviderClient, useChangeLocale, defi
},
);
-export type TFunction = Awaited>;
\ No newline at end of file
+export type TFunction = Awaited>;
diff --git a/locales/ko.ts b/locales/ko.ts
new file mode 100644
index 00000000..01a18601
--- /dev/null
+++ b/locales/ko.ts
@@ -0,0 +1,875 @@
+export default {
+ programs: {
+ available_programs: "이용 가능한 프로그램",
+ exercises_in_session: "이번 세션의 운동",
+ start_session: "세션 시작하기",
+ starting_session: "시작하는 중...",
+ more_than: "이상",
+ my_progress: "내 진행 상황",
+ session: "세션",
+ completed_feminine: "완료됨",
+ completed_sets: "완료한 세트",
+ "set#zero": "세트",
+ "set#one": "세트",
+ "set#other": "세트",
+ error_starting_session: "세션 시작 오류",
+ premium_session: "프리미엄 세션",
+ premium_session_description: "이 세션은 프리미엄 콘텐츠에 포함됩니다. 세부 정보를 볼 수는 있지만, 운동을 수행할 수는 없습니다.",
+ premium_session_exercises: "포함된 운동",
+ workout_description: "운동 설명",
+ connect_to_access: "접속하여 이용하기",
+ become_premium: "프리미엄 되기",
+ back_to_program: "프로그램으로 돌아가기",
+ no_equipment: "장비 불필요",
+ workout_programs_title: "운동 프로그램 (+ 진행 중)",
+ workout_programs: "운동 프로그램",
+ workout_programs_description: "나만의 도전을 선택하고 더 강해지세요! 💪",
+ no_programs_available: "이용 가능한 프로그램 없음",
+ no_programs_available_description: "프로그램이 곧 제공될 예정입니다!",
+ completed: "완료됨",
+ about: "정보",
+ program: "프로그램",
+ not_found: "프로그램을 찾을 수 없습니다",
+ characteristics: "특징",
+ weeks: "주",
+ sessions_per_week: "세션/주",
+ session_duration: "분/세션",
+ "your_coach#zero": "당신의 쿨 코치",
+ "your_coach#one": "당신의 쿨 코치",
+ "your_coach#other": "당신의 쿨 코치들",
+ community: "활성 커뮤니티",
+ community_count: "명의 쿨빌더가 함께합니다",
+ week_short: "주차",
+ week: "주",
+ exercises: "운동",
+ min_short: "분",
+ premium: "프리미엄",
+ free: "무료",
+ join_cta: "도전 참여하기",
+ continue: "계속하기",
+ sessions: "세션",
+ auth_required: "인증 필요",
+ auth_required_description: "이 운동 세션을 이용하려면 로그인해야 합니다.",
+ login_to_continue: "로그인하여 계속하기",
+ signup_to_continue: "회원가입하여 계속하기",
+ premium_required: "프리미엄 필요",
+ upgrade_to_premium: "프리미엄으로 업그레이드",
+ program_completed: "프로그램 완료",
+ check_out_program: "이 운동 프로그램을 확인해 보세요!",
+ share_success: "성공적으로 공유했습니다!",
+ copied_to_clipboard: "링크가 복사되었습니다!",
+ share_failed: "공유 실패",
+ premium_required_description: "프리미엄 전용 콘텐츠입니다. 모든 프리미엄 콘텐츠를 이용하려면 업그레이드하세요.",
+ important_info: "중요 정보",
+ donation_teaser:
+ "처음에는 후원금으로 운영되었지만, 개발 및 운영 비용을 충당하기에는 부족했습니다. 그래서 저희가 계속해서 서비스를 제공하고 더 많은 기능을 제공할 수 있도록 패키지를 만들었습니다.",
+ new: "NEW",
+ more_programs_coming_title: "더 많은 프로그램이 곧 출시됩니다!",
+ more_programs_coming_description:
+ "새로운 프로그램을 만들기 위해 열심히 노력하고 있습니다. 지금 프리미엄으로 업그레이드하시면 모든 프로그램을 자동으로 이용할 수 있습니다. 지원해 주셔서 감사합니다. 🚀",
+ coming_strength: "근력 & 근육",
+ coming_cardio: "유산소 HIIT",
+ coming_yoga: "요가 & 유연성",
+ sessions_coming_soon: "세션이 곧 공개됩니다!",
+ sessions_in_creation: "저희 팀이 이번 주를 위한 고품질 세션을 만드는 중입니다. 곧 다시 방문해 주세요! 🚀",
+ welcome_modal: {
+ welcome_title: "{programTitle}에 오신 것을 환영합니다!",
+ subtitle: "한계를 뛰어넘을 준비를 하세요! 💪",
+ level_label: "난이도",
+ duration_label: "기간",
+ frequency_label: "빈도",
+ later_button: "나중에",
+ start_button: "시작하기!",
+ },
+ },
+ premium: {
+ checkout_error: "결제 중 오류가 발생했습니다",
+ premium_required_title: "프리미엄 필요",
+ premium_required_subtitle: "프리미엄 전용 콘텐츠입니다. 모든 프리미엄 콘텐츠를 이용하려면 업그레이드하세요.",
+ premium_required_button: "프리미엄으로 업그레이드",
+ premium_active_title: "프리미엄 활성",
+ premium_active_subtitle: "모든 기능이 잠금 해제되었습니다",
+ free_intro_title: "이미 많은 기능을 무료로 이용하고 계십니다...",
+ free_intro_text:
+ "Workout.cool은 60,000명 이상의 사용자가 매일 이용하는 무료 오픈소스 피트니스 앱입니다. (VC 자본 없이^^) 애정을 담아 만들고 있으며, 운영을 위해 실제 시간과 비용이 소요됩니다.",
+ donation_story_text:
+ "처음에는 후원금으로 운영되었지만, 개발 및 운영 비용을 충당하기에는 부족했습니다. 그래서 저희가 계속해서 서비스를 제공하고 더 많은 기능을 제공할 수 있도록 패키지를 만들었습니다.",
+ health_upgrade_text: "만약 Workout.cool이 당신의 건강을 한 단계 발전시키는 데 도움이 되었다면, 프리미엄 전환을 고려해 주세요! :D",
+ unlock_features_text: "고급 기능을 잠금 해제하고 오픈소스 피트니스를 지원해 주세요.",
+ invest_yourself_quote: "운동과 책에는 절대 돈을 아끼지 마세요 — 자신에게 투자하는 것입니다!",
+ support_mission: "미션 지원하기",
+ best_value_badge: "최고의 혜택",
+ annual_plan: "연간",
+ monthly_plan: "월간",
+ discount_badge: "40% 할인",
+ per_month: "/월",
+ feature_all_programs: "모든 운동 프로그램",
+ feature_progress_tracking: "진행 상황 추적",
+ coming_soon: "(출시 예정)",
+ feature_future_updates: "향후 모든 프로그램 및 업데이트",
+ feature_priority_support: "우선 지원",
+ save_yearly: "연간 40% 절약",
+ processing: "처리 중...",
+ cta_annual: "지원하고 40% 절약하기",
+ cta_monthly: "전체 플랜 잠금 해제하기",
+ thank_supporting: "지원해 주셔서 감사합니다.",
+ no_pressure: "부담 갖지 마세요. 언제든지 업그레이드할 수 있습니다.",
+ keep_pushing: "계속해서 나아가세요! 후후",
+ still_unsure: "아직 고민되시나요? 걱정 마세요. Workout.cool은 항상 무료 오픈소스로 유지됩니다.",
+ support_helps: "하지만 저희가 만드는 서비스의 가치를 믿고 후원할 여유가 되신다면, 여러분의 지원이 큰 도움이 됩니다. 💚",
+ self_hosting: "자체 호스팅",
+ community: "커뮤니티",
+ mit_license: "MIT 라이선스",
+ pricing_year: "년",
+ pricing_month: "개월",
+ conversion_flow_title: "리디렉션 중...",
+ conversion_flow_message: "로그인에 성공했습니다! 결제 페이지로 이동합니다...",
+ redirecting_to_checkout: "결제 페이지로 이동 중",
+ },
+ breadcrumbs: {
+ home: "홈",
+ },
+ bottom_navigation: {
+ programs: "프로그램",
+ programs_tooltip: "프로그램 찾아보기",
+ workouts: "운동",
+ workouts_tooltip: "나만의 운동 만들기",
+ premium: "프리미엄",
+ premium_tooltip: "프리미엄으로 업그레이드",
+ tools: "도구",
+ tools_tooltip: "도구 찾아보기",
+ profile: "프로필",
+ profile_tooltip: "프로필 보기",
+ },
+ tools: {
+ try_now: "지금 사용해 보기",
+ title: "피트니스 도구",
+ subtitle: "당신의 훈련과 영양을 최적화하는 필수 계산기",
+ moreComingSoon: "더 많은 도구가 곧 출시됩니다",
+ meta: {
+ title: "피트니스 도구 - 훈련 & 영양 계산기 | Workout.cool",
+ description: "무료 피트니스 계산기: TDEE, 매크로, BMI, 심박수 구간, 1RM 등. 우리의 필수 도구로 훈련과 영양을 최적화하세요.",
+ keywords: "피트니스 계산기, 칼로리 계산기, 매크로 계산기, BMI 계산기, TDEE 계산기, 심박수 구간, 1RM, 피트니스 도구",
+ },
+ "calorie-calculator": {
+ body_fat_percentage: "체지방률",
+ body_fat_info_title:
+ "체지방률은 Katch-McArdle 및 Cunningham 공식에서 순수 체지방을 기준으로 계산하므로 필수적입니다. 정확한 체지방률을 모르는 경우, 온라인 시각 가이드 또는 DEXA 스캔을 사용하여 정확도를 높이세요.",
+ title: "칼로리 계산기",
+ description: "활동 수준과 목표에 따른 일일 칼로리 필요량(TDEE)을 계산하세요",
+ meta: {
+ title: "칼로리 계산기 - TDEE & 일일 칼로리 필요량 | Workout.cool",
+ description:
+ "일일 총 에너지 소비량(TDEE)과 일일 칼로리 필요량을 계산하세요. 체중 감량, 유지 또는 근육 증가에 대한 맞춤형 권장 사항을 받아보세요.",
+ keywords: "칼로리 계산기, TDEE 계산기, 일일 칼로리, 체중 감량 계산기, 칼로리 필요량, BMR 계산기, 신진대사량 계산기",
+ },
+ subtitle: "Mifflin-St Jeor 방정식을 기반으로 일일 칼로리 필요량을 계산하세요",
+ how_it_works: "이 계산기는 어떻게 작동하나요?",
+ how_it_works_description:
+ "이 계산기는 과학적으로 검증된 공식을 사용하여 개인의 특성과 생활 방식에 따라 일일 칼로리 필요량을 추정합니다.",
+ how_it_works_step1: "기초 신진대사량(휴식 시 소모되는 칼로리)을 계산합니다",
+ how_it_works_step2: "활동 수준에 따라 조정합니다",
+ how_it_works_step3: "목표(체중 감량, 유지, 증가)에 따라 개인화합니다",
+ calculate: "계산하기",
+ calculating: "계산 중...",
+ tap_info_icons: "더 많은 정보는 ℹ️ 아이콘을 탭하세요",
+ gender: "성별",
+ male: "남성",
+ female: "여성",
+ units: "단위",
+ metric: "미터법",
+ imperial: "영국식",
+ age: "나이",
+ age_placeholder: "나이를 입력하세요",
+ years: "세",
+ height: "키",
+ height_placeholder: "키를 입력하세요",
+ weight: "몸무게",
+ weight_placeholder: "몸무게를 입력하세요",
+ cm: "cm",
+ kg: "kg",
+ lbs: "lbs",
+ feet: "피트",
+ inches: "인치",
+ activity_level: "활동 수준",
+ activity: {
+ sedentary: "비활동적",
+ sedentary_desc: "거의 또는 전혀 운동하지 않음, 사무직, 최소한의 걷기",
+ light: "가벼운 활동",
+ light_desc: "주 1-3일 가벼운 운동 또는 매일 걷기",
+ moderate: "보통 활동",
+ moderate_desc: "주 3-5일 보통 운동, 활동적인 라이프스타일",
+ active: "매우 활동적",
+ active_desc: "주 6-7일 격렬한 운동, 매우 활동적인 직업",
+ very_active: "극도로 활동적",
+ very_active_desc: "선수, 신체 활동이 많은 직업 + 매일 훈련",
+ },
+ goal: "목표",
+ goals: {
+ lose_fast: "빠른 체중 감량",
+ lose_fast_desc: "주당 1kg 감량 - 공격적이지만 효과적",
+ lose_slow: "체중 감량",
+ lose_slow_desc: "주당 0.5kg 감량 - 지속 가능하고 건강함",
+ maintain: "체중 유지",
+ maintain_desc: "현재 체중 유지 - 몸매 유지를 위한 완벽한 선택",
+ gain_slow: "체중 증가",
+ gain_slow_desc: "주당 0.5kg 증가 - 깨끗한 근육 증가",
+ gain_fast: "빠른 체중 증가",
+ gain_fast_desc: "주당 1kg 증가 - 최대 근육 성장",
+ },
+ results: {
+ title: "결과",
+ bmr: "기초대사량(BMR)",
+ bmr_explanation:
+ "기초대사량(BMR)은 호흡, 혈액 순환, 세포 생성 등 기본적인 기능을 유지하기 위해 완전한 휴식 상태에서 신체가 소모하는 칼로리 양입니다. 이는 신체가 생존하는 데 필요한 최소한의 에너지입니다.",
+ tdee: "총 일일 에너지 소비량(TDEE)",
+ tdee_explanation:
+ "총 일일 에너지 소비량(TDEE)은 기초대사량(BMR)에 일상 활동과 운동을 통해 소모되는 칼로리를 더한 값입니다. 이는 활동 수준에 따라 하루에 소모하는 총 칼로리 양입니다.",
+ target: "목표 칼로리",
+ macros: "권장 매크로",
+ macros_explanation:
+ "매크로(다량 영양소)는 신체에 필요한 세 가지 주요 영양소 그룹입니다: 단백질(근육 생성 및 회복), 탄수화물(에너지), 지방(호르몬 및 비타민 흡수). 표시된 비율은 대부분의 피트니스 목표에 적합한 균형 잡힌 구성입니다.",
+ protein: "단백질",
+ carbs: "탄수화물",
+ fat: "지방",
+ disclaimer:
+ "이 계산은 평균 공식을 기반으로 한 추정치입니다. 실제 칼로리 필요량은 개인의 요인에 따라 다를 수 있습니다. 맞춤형 조언을 원하시면 의료 전문가 또는 공인 영양사와 상담하세요.",
+ },
+ faq: {
+ title: "자주 묻는 질문",
+ q1: "다른 계산기와 칼로리 목표가 다른 이유는 무엇인가요?",
+ a1: "다른 계산기는 서로 다른 공식이나 활동 계수를 사용할 수 있습니다. 저희는 대부분의 사람들에게 가장 정확하다고 알려진 Mifflin-St Jeor 공식을 사용합니다. 그러나 개인의 신진대사량은 이 추정치와 10-20% 정도 차이가 날 수 있습니다.",
+ q2: "매일 정확히 이만큼의 칼로리를 섭취해야 하나요?",
+ a2: "이 수치는 일일 평균 목표치입니다. 어떤 날은 조금 더 먹고 어떤 날은 덜 먹는 것은 정상입니다. 매일 정확하게 맞추려 하기보다 주간 평균에 집중하세요. 몸의 배고픔과 포만감 신호에 귀 기울이세요.",
+ q3: "이 권장 사항을 따랐는데도 결과가 나타나지 않으면 어떻게 해야 하나요?",
+ a3: "2~3주 후에도 결과가 나타나지 않으면 조절이 필요할 수 있습니다. 실제 신진대사량이 계산된 수치보다 높거나 낮을 수 있습니다. 100~200칼로리를 조절하고 2주 더 모니터링하세요. 또한 음식 섭취량을 정확하게 기록하고 있는지 확인하세요.",
+ q4: "매크로 권장 사항은 모든 사람에게 적합한가요?",
+ a4: "30/40/30 비율(단백질/탄수화물/지방)은 대부분의 사람에게 적합한 균형 잡힌 접근법입니다. 그러나 운동선수, 질환이 있는 사람, 또는 특정 식단(키토, 비건 등)을 따르는 사람은 다른 비율이 필요할 수 있습니다. 맞춤형 권장 사항은 영양사와 상담하세요.",
+ },
+ },
+ "macro-calculator": {
+ title: "매크로 계산기",
+ description: "피트니스 목표에 맞는 최적의 단백질, 탄수화물, 지방 비율을 찾아보세요",
+ },
+ "bmi-calculator": {
+ title: "BMI 계산기",
+ description: "체질량지수(BMI)를 계산하고 체중 범주를 확인하세요",
+ },
+ "heart-rate-calculator": {
+ title: "심박수 구간",
+ description: "지방 연소와 운동 효율을 위한 최적의 훈련 구간을 확인하세요",
+ },
+ "one-rep-max": {
+ title: "1RM 계산기",
+ description: "1RM(최대 1회 반복)을 추정하고 근력 훈련 비율을 계획하세요",
+ },
+ back_to_calculators: "계산기 목록으로 돌아가기",
+ body_fat_percentage: "체지방률",
+ body_fat_info_title: "체지방률이란 무엇인가요?",
+ body_fat_info_content:
+ "체지방률은 Katch-McArdle 및 Cunningham 공식에서 순수 체지방을 기준으로 계산하므로 필수적입니다. 정확한 체지방률을 모르는 경우, 온라인 시각 가이드 또는 DEXA 스캔을 사용하여 정확도를 높이세요.",
+ "calorie-calculator-hub": {
+ title: "칼로리 계산 공식",
+ subtitle: "필요에 맞는 최적의 공식을 선택하고 정확한 칼로리를 계산하세요",
+ meta: {
+ title: "칼로리 계산 공식 - BMR & TDEE 계산기 | Workout.cool",
+ description:
+ "Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham, Oxford 등 다양한 BMR 공식을 비교해 보세요. 당신에게 가장 적합한 칼로리 계산기를 선택하세요.",
+ keywords: "BMR 공식, 칼로리 계산기 비교, Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham, Oxford, TDEE 계산기",
+ },
+ which_formula: "어떤 공식을 선택해야 하나요?",
+ formula_explanation: "공식마다 특정 사용자에게 더 잘 맞습니다. 다음 가이드를 참고하여 선택하세요:",
+ recommendation_general: "전체적으로 가장 정확하며, 일반인에게 가장 적합한 공식",
+ recommendation_traditional: "널리 사용되지만 정확도는 다소 떨어지는 고전 공식",
+ recommendation_bodyfat: "체지방률을 아는 경우 가장 정확한 공식",
+ since: "발표 연도",
+ all_formulas: "모든 공식",
+ popularity: "인기도",
+ accuracy: "정확도",
+ accuracy_high: "높음",
+ accuracy_good: "좋음",
+ accuracy_medium: "보통",
+ best_for: "적합한 대상",
+ best_for_general: "일반 사용자",
+ best_for_traditional: "고전적 접근",
+ best_for_athletes: "운동선수",
+ best_for_bodybuilders: "보디빌더",
+ best_for_european: "유럽인",
+ best_for_comparison: "모두 비교",
+ "mifflin-st-jeor": {
+ title: "Mifflin-St Jeor (권장)",
+ description: "1990년에 개발된 일반인에게 가장 정확한 공식입니다. 현재 BMR 계산의 표준으로 여겨집니다.",
+ },
+ "harris-benedict": {
+ title: "Harris-Benedict (고전)",
+ description: "고전 공식의 1984년 개정 버전입니다. 널리 사용되지만 일부 사람에게는 칼로리를 과대평가하는 경향이 있습니다.",
+ },
+ "katch-mcardle": {
+ title: "Katch-McArdle (운동선수)",
+ description: "순수 체지방을 기반으로 합니다. 체지방률을 알고 신체 활동이 활발한 사람에게 가장 정확합니다.",
+ },
+ cunningham: {
+ title: "Cunningham (보디빌더)",
+ description: "체지방이 낮은 매우 마른 운동선수와 보디빌더를 위해 설계되었습니다. 순수 체지방 계산을 사용합니다.",
+ },
+ oxford: {
+ title: "Oxford (유럽인)",
+ description: "유럽 인구를 기반으로 한 최신 공식(2005)입니다. 나이 구간을 고려합니다.",
+ },
+ comparison: {
+ title: "모든 공식 비교",
+ description: "모든 공식의 결과를 나란히 비교하여 차이점을 확인하고 자신에게 가장 적합한 공식을 선택하세요.",
+ },
+ },
+ "mifflin-st-jeor": {
+ title: "Mifflin-St Jeor 계산기",
+ subtitle: "BMR 계산의 표준 - 일반인에게 가장 정확합니다",
+ meta: {
+ title: "Mifflin-St Jeor 계산기 - 가장 정확한 BMR & TDEE | Workout.cool",
+ description:
+ "Mifflin-St Jeor 방정식을 사용하여 BMR과 TDEE를 계산하세요. 일반인에게 가장 정확한 공식으로, 맞춤형 칼로리 권장 사항을 제공합니다.",
+ keywords: "Mifflin-St Jeor 계산기, BMR 계산기, TDEE 계산기, 가장 정확한 칼로리 계산기, 신진대사량 계산기",
+ },
+ how_it_works: "Mifflin-St Jeor 공식의 작동 방식",
+ how_it_works_description:
+ "1990년에 개발된 이 공식은 건강한 성인의 기초대사량(BMR)을 계산하는 데 가장 정확하다고 알려져 있습니다. Harris-Benedict 공식보다 더 정밀하며 영양사와 피트니스 전문가들이 널리 추천합니다.",
+ },
+ "harris-benedict": {
+ title: "Harris-Benedict 계산기",
+ subtitle: "고전 BMR 공식 - 칼로리 계산의 전통적인 접근 방식",
+ meta: {
+ title: "Harris-Benedict 계산기 - 고전 BMR & TDEE 공식 | Workout.cool",
+ description:
+ "개정된 Harris-Benedict 방정식(1984)을 사용하여 BMR과 TDEE를 계산하세요. 현대적인 칼로리 계산의 시작점이 된 고전 공식입니다.",
+ keywords: "Harris-Benedict 계산기, 고전 BMR 계산기, 전통 TDEE 계산기, 개정된 Harris-Benedict 공식",
+ },
+ how_it_works: "Harris-Benedict 공식의 작동 방식",
+ how_it_works_description:
+ "1919년에 처음 개발되어 1984년에 개정된 Harris-Benedict 방정식은 BMR을 계산하는 최초의 공식 중 하나였습니다. 최신 공식보다는 정확도가 약간 떨어지지만, 여전히 널리 사용되며 대부분의 사람들에게 좋은 추정치를 제공합니다.",
+ },
+ "katch-mcardle": {
+ title: "Katch-McArdle 계산기",
+ subtitle: "순수 체지방 기반의 정밀한 BMR 계산 - 운동선수에게 이상적",
+ meta: {
+ title: "Katch-McArdle 계산기 - 순수 체지방 BMR & TDEE | Workout.cool",
+ description:
+ "순수 체지방을 기반으로 한 Katch-McArdle 공식을 사용하여 BMR과 TDEE를 계산하세요. 체지방률을 아는 사람에게 가장 정확합니다.",
+ keywords: "Katch-McArdle 계산기, 순수 체지방 BMR, 체지방률 계산기, 운동선수 BMR 계산기, 정밀한 TDEE",
+ },
+ how_it_works: "Katch-McArdle 공식의 작동 방식",
+ how_it_works_description:
+ "이 공식은 총 체중 대신 순수 체지방을 기반으로 BMR을 계산하여, 체지방률을 아는 사람에게 더 정확합니다. 특히 운동선수와 신체 활동이 활발한 사람에게 유용합니다.",
+ },
+ cunningham: {
+ title: "Cunningham 계산기",
+ subtitle: "체지방이 매우 낮은 운동선수와 보디빌더를 위해 설계된 BMR 공식",
+ meta: {
+ title: "Cunningham 계산기 - 마른 운동선수 & 보디빌더를 위한 BMR | Workout.cool",
+ description: "체지방이 낮은 매우 마른 운동선수와 보디빌더를 위해 특별히 설계된 Cunningham 공식을 사용하여 BMR과 TDEE를 계산하세요.",
+ keywords: "Cunningham 계산기, 보디빌더 BMR 계산기, 마른 운동선수 BMR, 낮은 체지방 BMR 계산기, 대회 준비 계산기",
+ },
+ how_it_works: "Cunningham 공식의 작동 방식",
+ how_it_works_description:
+ "체지방률이 낮은 매우 마른 개인을 위해 특별히 개발된 이 공식은 다른 방정식보다 높은 BMR 추정치를 제공합니다. 시합을 준비하는 경쟁 운동선수 및 보디빌더에게 가장 정확합니다.",
+ },
+ oxford: {
+ title: "Oxford 계산기",
+ subtitle: "나이를 고려한 유럽 인구 기반의 최신 BMR 공식",
+ meta: {
+ title: "Oxford 계산기 - 최신 BMR & TDEE 공식 | Workout.cool",
+ description: "나이에 따른 계산을 포함하는 유럽 인구 기반의 최신 공식인 Oxford 방정식(2005)을 사용하여 BMR과 TDEE를 계산하세요.",
+ keywords: "Oxford 계산기, 최신 BMR 계산기, 유럽 BMR 공식, 연령별 BMR 계산기, 2005 BMR 공식",
+ },
+ how_it_works: "Oxford 공식의 작동 방식",
+ how_it_works_description:
+ "2005년에 발표된 이 공식은 최신 BMR 공식 중 하나입니다. 유럽 인구 데이터를 사용하여 개발되었으며, 30세 미만과 이상에 대한 다른 방정식을 제공하여 나이 구간을 고려합니다.",
+ },
+ "calorie-calculator-comparison": {
+ title: "모든 BMR 공식 비교",
+ subtitle: "다양한 BMR 공식이 어떻게 칼로리 필요량을 계산하는지 나란히 비교해 보세요",
+ meta: {
+ title: "BMR 공식 비교 - 모든 칼로리 계산기 비교 | Workout.cool",
+ description:
+ "Mifflin-St Jeor, Harris-Benedict, Katch-McArdle, Cunningham, Oxford BMR 공식을 나란히 비교하세요. 어떤 공식이 당신에게 가장 적합한지 확인해 보세요.",
+ keywords: "BMR 공식 비교, 칼로리 계산기 비교, Mifflin vs Harris-Benedict, 최적 BMR 계산기, 칼로리 공식 비교",
+ },
+ how_it_works: "이 비교는 어떻게 작동하나요?",
+ how_it_works_description:
+ "한 번만 정보를 입력하면 모든 주요 BMR 공식이 일일 칼로리 필요량을 어떻게 계산하는지 확인할 수 있습니다. 이를 통해 차이점을 이해하고 목표에 가장 적합한 공식을 선택할 수 있습니다.",
+ input_details: "당신의 정보",
+ compare: "비교하기",
+ results_comparison: "공식 비교 결과",
+ vs_mifflin: "Mifflin-St Jeor 대비",
+ summary: "요약 및 권장 사항",
+ summary_explanation: "공식마다 다른 결과가 나올 수 있습니다. 일반적으로 ±100~200 칼로리의 차이는 정상적이며 예상되는 범위입니다.",
+ recommendation:
+ "대부분의 사람들에게 Mifflin-St Jeor가 가장 정확한 기준을 제공합니다. 운동선수는 체지방률을 알고 있다면 Katch-McArdle을 고려해야 합니다.",
+ },
+ },
+ levels: {
+ BEGINNER: "초급",
+ INTERMEDIATE: "중급",
+ ADVANCED: "고급",
+ },
+ email_sent: "이메일 전송 완료",
+ cant_send_email: "이메일을 보낼 수 없습니다",
+ logout: "로그아웃",
+ verify_email: "이메일을 인증해 주세요. ⚠️ 스팸 메일함을 확인하는 것을 잊지 마세요.",
+ verify_email_subtitle: "계속하려면 이메일을 인증해 주세요.",
+ resend_email: "이메일 재전송",
+ resend_email_countdown: "{seconds}초 후에 이메일 재전송",
+ signin_error_subtitle: "자격 증명을 확인하고 다시 시도해 주세요.",
+ register_title: "계정 만들기",
+ register_description: "계정을 만들려면 아래에 정보를 입력하세요",
+ register_terms: "가입함으로써 귀하는 다음 사항에 동의합니다:",
+ register_privacy: "개인정보 처리방침",
+ register_privacy_link: "및",
+ register_privacy_link_2: "개인정보 처리방침",
+ password_forgot_title: "비밀번호를 잊으셨나요?",
+ password_forgot_subtitle: "비밀번호를 재설정하려면 이메일을 입력하세요",
+ new_password: "새 비밀번호",
+ new_password_placeholder: "새 비밀번호를 입력하세요",
+ current_password: "현재 비밀번호",
+ current_password_placeholder: "현재 비밀번호를 입력하세요",
+ confirm_password: "비밀번호 확인",
+ confirm_password_placeholder: "비밀번호를 다시 입력하세요",
+
+ success: {
+ feedback_sent: "피드백 전송 완료",
+ password_forgot_success: "이메일이 전송되었습니다",
+ reset_password_success: "비밀번호가 성공적으로 재설정되었습니다",
+ password_updated_successfully: "비밀번호가 성공적으로 업데이트되었습니다",
+ },
+
+ error: {
+ invalid_credentials: "자격 증명이 유효하지 않거나 계정이 존재하지 않습니다",
+ upload_failed: "업로드 실패",
+ generic_error: "작업 중 오류가 발생했습니다",
+ sending_email: "이메일 전송 오류",
+ },
+
+ backend_errors: {
+ EMAIL_ALREADY_EXISTS: "이미 존재하는 이메일입니다",
+ INVALID_FILE_TYPE: "유효하지 않은 파일 형식입니다",
+ FILE_TOO_LARGE: "파일 크기가 너무 큽니다",
+ NO_FILE_UPLOADED: "업로드된 파일이 없습니다",
+ IMAGE_PROCESSING_ERROR: "이미지 처리 오류",
+ upload_failed: "업로드 실패",
+ },
+
+ profile: {
+ new_workout: "새 운동",
+ alert: {
+ title: "진행 상황은 브라우저에 저장됩니다.",
+ create_account: "계정 만들기",
+ log_in: "로그인",
+ to_ensure_it_is_not_getting_lost: "로 잃어버리지 않도록 하세요.",
+ },
+ },
+
+ release_notes: {
+ title: "새로운 소식",
+ release_notes: "릴리스 노트",
+ notes: {
+ note_2025_06_23: {
+ title: "🇵🇹 포르투갈어 지원 및 후원 배너",
+ content:
+ "이제 앱이 포르투갈어를 지원합니다! 또한 GitHub Sponsors 또는 Ko-fi를 통한 프로젝트의 지속적인 운영 비용을 지원하기 위해 후원 배너를 추가했습니다. 🙏",
+ },
+ note_2025_06_22: {
+ title: "🌍 새로운 언어 및 성능 향상!",
+ content: "이제 앱이 중국어와 러시아어로 제공됩니다! 또한 더욱 부드러운 사용 경험을 위해 드래그 앤 드롭 성능을 개선했습니다. ⚡",
+ },
+ note_2025_06_19: {
+ title: "📱 이제 PWA로 사용 가능!",
+ content: "Workout.cool v1.2가 Progressive Web App이 되었습니다! 오프라인 접근으로 네이티브 앱 경험을 위해 휴대폰에 설치하세요. 🚀",
+ },
+ note_2025_06_18: {
+ title:
+ "🚀 Hacker News에서 1위 달성!",
+ content: "Workout.cool이 Hacker News에서 최고 순위에 올랐습니다! 놀라운 지원에 감사드리며 모든 신규 사용자분들을 환영합니다! 💪",
+ },
+ note_2025_06_01: {
+ title: "🎉 새로운 기능: 릴리스 노트 대화 상자",
+ content: "이제 헤더에서 새로운 소식을 바로 확인할 수 있습니다! 더 많은 업데이트를 기대해 주세요.",
+ },
+ note_2025_05_20: {
+ title: "UI 개선",
+ content: "모바일 반응성이 개선되었고, 버튼에 미세한 호버 효과가 추가되었습니다.",
+ },
+ },
+ },
+
+ // Donation Alert
+ donation_alert: {
+ title: "Workout.cool을 무료로 유지하려면 후원해주세요:",
+ or: "또는",
+ },
+
+ // Donation Modal
+ donation_modal: {
+ support_via: "후원 방법...",
+ title: "프로젝트 후원하기",
+ congrats: "운동 완료를 축하합니다! 🎉",
+ subtitle: "이 앱은 무료로 제공되지만, 저에게는 실제 비용이 발생합니다...",
+ costs_title: "비용에 대한 현실",
+ costs_description: "현재 후원금만으로는 서버, 인증, 인프라, 데이터베이스 등과 같은 기본 비용조차 충당되지 않습니다.",
+ open_source_title: "100% 오픈소스",
+ open_source_description:
+ "이 앱은 완전 무료이며, 광고가 없고, 오픈소스입니다. 수익을 창출하지 않으며, 커뮤니티를 돕고 사람들이 운동하도록 돕는 열정적인 프로젝트입니다.",
+ no_ads: "광고 없음",
+ no_tracking: "추적 없음",
+ impact_title: "당신의 영향력",
+ impact_3_euros: "• 3유로만으로도 1주일 서버 비용을 충당할 수 있습니다",
+ impact_support: "• 당신의 후원은 앱을 모두에게 무료로 유지할 수 있게 합니다",
+ impact_footer: "작은 후원이라도 실질적인 변화를 만듭니다! 🙏",
+ later_button: "나중에",
+ support_button: "프로젝트 후원하기",
+ },
+
+ // Contact Support
+ contact_support: "지원팀에 문의하기",
+ contact_support_subtitle: "문제를 자세히 설명해 주시면 최대한 빨리 도와드리겠습니다. 다음 주소로 직접 문의하셔도 됩니다:",
+
+ // Social Platforms
+ social_platforms: {
+ x: "X (트위터)",
+ facebook: "페이스북",
+ email: "이메일",
+ whatsapp: "왓츠앱",
+ website: "웹사이트",
+ phone: "전화",
+ youtube: "유튜브",
+ linkedin: "링크드인",
+ snapchat: "스냅챗",
+ instagram: "인스타그램",
+ tiktok: "틱톡",
+ threads: "스레드",
+ },
+
+ // Workout Builder
+ workout_builder: {
+ confirm_delete: "이 운동 세션을 정말로 삭제하시겠습니까?",
+ steps: {
+ equipment: {
+ title: "장비",
+ description: "장비를 선택하세요",
+ },
+ muscles: {
+ title: "근육",
+ description: "훈련 부위를 선택하세요",
+ },
+ exercises: {
+ title: "운동",
+ description: "운동을 맞춤 설정하세요",
+ },
+ },
+ muscles: {
+ back: "등",
+ abdominals: "복근",
+ abductors: "외전근",
+ adductors: "내전근",
+ biceps: "이두근",
+ triceps: "삼두근",
+ chest: "가슴",
+ shoulders: "어깨",
+ quadriceps: "대퇴사두근",
+ hamstrings: "햄스트링",
+ glutes: "둔근",
+ calves: "종아리",
+ forearms: "전완근",
+ traps: "승모근",
+ obliques: "외복사근",
+ },
+ exercise: {
+ watch_video: "영상 보기",
+ shuffle: "섞기",
+ pick: "선택",
+ remove: "제거",
+ no_video_available: "영상이 없습니다.",
+ },
+ loading: {
+ exercises: "운동을 불러오는 중...",
+ },
+ error: {
+ loading_exercises: "운동을 불러오는 중 오류가 발생했습니다",
+ },
+ no_exercises_found: "운동을 찾을 수 없습니다. 장비나 근육 선택을 변경해 보세요.",
+ equipment: {
+ bodyweight: {
+ label: "맨몸",
+ description: "오직 맨몸만을 사용하는 운동",
+ },
+ dumbbell: {
+ label: "덤벨",
+ description: "덤벨을 이용한 프리 웨이트 운동",
+ },
+ barbell: {
+ label: "바벨",
+ description: "바벨을 이용한 복합 운동",
+ },
+ kettlebell: {
+ label: "케틀벨",
+ description: "케틀벨을 이용한 동적 운동",
+ },
+ band: {
+ label: "밴드",
+ description: "저항 밴드 운동",
+ },
+ plate: {
+ label: "원판",
+ description: "원판을 사용하는 운동",
+ },
+ pullup_bar: {
+ label: "풀업 바",
+ description: "풀업 바를 이용한 상체 운동",
+ },
+ bench: {
+ label: "벤치",
+ description: "벤치를 이용한 운동 및 지지",
+ },
+ },
+ navigation: {
+ previous: "이전",
+ continue: "계속",
+ complete: "완료",
+ },
+ stats: {
+ "muscle_selected#zero": "0개 근육 선택됨",
+ "muscle_selected#one": "1개 근육 선택됨",
+ "muscle_selected#other": "{count}개 근육 선택됨",
+ "equipment_selected#zero": "0개 장비 선택됨",
+ "equipment_selected#one": "1개 장비 선택됨",
+ "equipment_selected#other": "{count}개 장비 선택됨",
+ selected: "선택됨",
+ total: "총",
+ equipment_ready: "장비 준비 완료",
+ equipment_ready_plural: "장비 준비 완료",
+ },
+ selection: {
+ choose_your_arsenal: "장비를 선택하세요",
+ select_equipment_description: "장비를 선택하여 맞춤형 운동을 잠금 해제하세요",
+ clear_all: "모두 지우기",
+ muscle_selection_coming_soon: "근육 선택 (출시 예정)",
+ muscle_selection_description: "훈련하고 싶은 근육을 클릭하여 선택하세요.",
+ exercise_selection_coming_soon: "운동 선택 (출시 예정)",
+ exercise_selection_description: "이 단계에서는 맞춤형 운동 추천을 볼 수 있습니다.",
+ },
+ session: {
+ back_to_workout: "운동으로 돌아가기",
+ congrats: "축하합니다, 운동을 완료했습니다! 🎉",
+ congrats_subtitle: "해냈습니다!",
+ see_instructions: "설명 보기",
+ finish_set: "세트 완료",
+ finish_session: "세션 완료",
+ bodyweight: "맨몸",
+ weight: "중량",
+ reps: "횟수",
+ time: "시간",
+ next_exercise: "다음 운동",
+ add_set: "세트 추가",
+ add_column: "열 추가",
+ add_row: "행 추가",
+ remove_column: "열 제거",
+ set_number: "{number}세트",
+ set_number_plural: "{number}세트",
+ set_number_singular: "{number}세트",
+ set_number_plural_singular: "{number}세트",
+ workout_in_progress: "운동 진행 중",
+ started_at: "시작 시간",
+ quit_workout: "운동 종료",
+ elapsed_time: "경과 시간",
+ chronometer: "크로노미터",
+ exercise_progress: "운동 진행도",
+ total_volume: "총 볼륨",
+ current_exercise: "현재 운동",
+ complete: "완료",
+ active: "활성",
+ already_have_a_active_session: "이미 진행 중인 세션이 있습니다. 운동을 마치거나 종료해야만 다시 시작할 수 있습니다.",
+ no_exercise_selected: "선택된 운동이 없습니다",
+ quit_workout_title: "운동을 종료하시겠습니까?",
+ progress: "진행도",
+ quit_warning: "정말로 종료하시겠습니까? 진행 상황을 저장하거나 완전히 잃을 수 있습니다.",
+ save_and_quit: "저장 후 종료",
+ quit_without_save: "저장하지 않고 종료",
+ continue_workout: "운동 계속하기",
+ history: "운동 기록 [{count}]",
+ no_workout_yet: "아직 운동 기록이 없습니다.",
+ start: "시작",
+ end: "종료",
+ exercise: "운동",
+ repeat: "반복",
+ delete: "삭제",
+ },
+ attribute_value: {
+ bodyweight: "맨몸",
+ strength: "근력",
+ powerlifting: "파워리프팅",
+ calisthenic: "맨몸 운동",
+ plyometrics: "플라이오메트릭스",
+ stretching: "스트레칭",
+ strongman: "스트롱맨",
+ cardio: "유산소",
+ stabilization: "안정화",
+ power: "파워",
+ resistance: "저항",
+ crossfit: "크로스핏",
+ weightlifting: "역도",
+ neck: "목",
+ lats: "광배근",
+ adductors: "내전근",
+ abductors: "외전근",
+ groin: "사타구니",
+ full_body: "전신",
+ rotator_cuff: "회전근개",
+ hip_flexor: "고관절 굴곡근",
+ achilles_tendon: "아킬레스건",
+ fingers: "손가락",
+ smith_machine: "스미스 머신",
+ other: "기타",
+ ez_bar: "EZ바",
+ machine: "머신",
+ desk: "책상",
+ none: "없음",
+ cable: "케이블",
+ medicine_ball: "메디신볼",
+ swiss_ball: "스위스볼",
+ foam_roll: "폼롤러",
+ trx: "TRX",
+ box: "박스",
+ ropes: "로프",
+ spin_bike: "스핀 바이크",
+ step: "스텝 박스",
+ bosu: "보수볼",
+ tyre: "타이어",
+ sandbag: "샌드백",
+ pole: "봉",
+ wall: "벽",
+ bar: "바",
+ rack: "랙",
+ car: "자동차",
+ sled: "썰매",
+ chain: "체인",
+ skierg: "스키에르그",
+ rope: "로프",
+ na: "해당 없음",
+ isolation: "고립 운동",
+ compound: "복합 운동",
+ },
+ },
+ commons: {
+ signup_with: "{provider}으로 회원가입",
+ signin_with: "{provider}으로 로그인",
+ signup: "회원가입",
+ login: "로그인",
+ connecting: "연결 중...",
+ login_to_your_account_title: "계정에 로그인하세요",
+ login_to_your_account_subtitle: "로그인하려면 아래에 자격 증명을 입력하세요",
+ password_forgot: "비밀번호를 잊으셨나요?",
+ password_reset_success: "비밀번호가 성공적으로 재설정되었습니다",
+ dont_have_account: "계정이 없으신가요?",
+ already_have_account: "이미 계정이 있으신가요?",
+ or: "또는",
+ add: "추가",
+ your_feminine: "당신의",
+ password: "비밀번호",
+ email: "이메일",
+ logout: "로그아웃",
+ first_name: "이름",
+ last_name: "성",
+ verify_password: "비밀번호 확인",
+ submit: "제출",
+ upload: "업로드",
+ cancel: "취소",
+ save_changes: "변경 사항 저장",
+ change: "변경",
+ subject: "제목",
+ message: "메시지",
+ saving: "저장 중...",
+ edit: "수정",
+ more_options: "더 보기",
+ open_link: "링크 열기",
+ hide: "숨기기",
+ make_visible: "보이게 하기",
+ delete: "삭제",
+ share: "공유",
+ title: "제목",
+ subtitle: "부제목",
+ content: "내용",
+ save: "저장",
+ button: "버튼",
+ card: "카드",
+ go_back: "뒤로 가기",
+ next: "다음",
+ choose_image: "이미지 선택",
+ soon: "곧",
+ coming_soon_with_emoji: "출시 예정 🤫",
+ no_image: "이미지 없음",
+ description: "설명",
+ price: "가격",
+ duration: "기간",
+ location: "위치",
+ schedule: "일정",
+ participants_info: "참가자 정보",
+ description_placeholder: "설명을 입력하세요",
+ title_placeholder: "제목을 입력하세요",
+ changes_saved: "변경 사항이 저장되었습니다",
+ replace: "교체",
+ loading: "불러오는 중...",
+ image_deleted: "이미지가 삭제되었습니다",
+ discover_workoutcool: "Workout.cool 알아보기",
+ received_just_now: "방금 전 수신",
+ copied: "복사됨",
+ url_copied: "URL이 복사되었습니다",
+ copy_failed: "복사 실패",
+ accordion: "아코디언",
+ image: "이미지",
+ other: "기타",
+ register: "회원가입",
+ instantly: "즉시",
+ immediately: "즉시",
+ link: "링크",
+ accept: "동의",
+ deny: "거부",
+ invalid_input: "유효하지 않은 입력입니다. 오류를 확인하세요.",
+ copy_url: "URL 복사",
+ page_url: "페이지 URL",
+ saving_short: "저장 중...",
+ saved_short: "저장됨",
+ looks_like_you_are_lost: "길을 잃으신 것 같습니다",
+ the_page_you_are_looking_for_is_not_available: "찾으시는 페이지를 사용할 수 없습니다",
+ go_to_home: "홈으로 가기",
+ go_to_profile: "프로필로 가기",
+ terms: "이용약관",
+ privacy: "개인정보 처리방침",
+ sales_terms: "판매 약관",
+ consent_banner: "저희는 쿠키를 사용하여 경험을 개선합니다. 동의를 클릭하면 쿠키 사용에 동의하는 것입니다.",
+ about: "회사 소개",
+ profile: "프로필",
+ donate: "후원",
+ my_account: "내 계정",
+ dashboard: "대시보드",
+ home: "홈",
+ changelog: "변경사항",
+ stop_impersonation_button: "사용자 가장 중지",
+ impersonating_user_label: "사용자 가장 중",
+ re_hello: "다시 안녕하세요",
+ back_to_login: "로그인으로 돌아가기",
+ sending: "전송 중...",
+ send_me_link: "링크 보내기",
+ subscription: "구독",
+ manage_subscription: "구독 관리",
+ become_premium: "프리미엄 되기",
+ extremely_dissatisfied: "매우 불만족",
+ somewhat_dissatisfied: "다소 불만족",
+ neutral: "보통",
+ satisfied: "만족",
+ support: "지원",
+ change_language: "언어 변경",
+ in_progress: "진행 중",
+ premium: "프리미엄",
+ free: "무료",
+ new: "신규",
+ coming_soon: "출시 예정",
+ },
+} as const;
diff --git a/locales/server.ts b/locales/server.ts
index 577f7f6b..d25cc67e 100644
--- a/locales/server.ts
+++ b/locales/server.ts
@@ -6,5 +6,6 @@ export const { getI18n, getScopedI18n, getStaticParams } = createI18nServer({
es: () => import("./es"),
"zh-CN": () => import("./zh-CN"),
ru: () => import("./ru"),
- pt: () => import("./pt")
-});
\ No newline at end of file
+ pt: () => import("./pt"),
+ ko: () => import("./ko"),
+});
diff --git a/locales/types.ts b/locales/types.ts
index 0eabae97..0d016dcd 100644
--- a/locales/types.ts
+++ b/locales/types.ts
@@ -1,2 +1,2 @@
-export const locales = ["en", "fr", "es", "zh-CN", "ru", "pt"] as const;
+export const locales = ["en", "fr", "es", "zh-CN", "ru", "pt", "ko"] as const;
export type Locale = (typeof locales)[number];
diff --git a/middleware.ts b/middleware.ts
index 4bf0423c..17697935 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -18,7 +18,7 @@ function detectUserLocale(request: NextRequest): string {
.sort((a, b) => b.quality - a.quality);
// Map browser locales to supported locales
- const supportedLocales = ["en", "fr", "es", "zh-cn", "ru", "pt"];
+ const supportedLocales = ["en", "fr", "es", "zh-cn", "ru", "pt", "ko"];
for (const { locale } of languages) {
// Exact match
@@ -42,7 +42,7 @@ function detectUserLocale(request: NextRequest): string {
}
const I18nMiddleware = createI18nMiddleware({
- locales: ["en", "fr", "es", "zh-CN", "ru", "pt"],
+ locales: ["en", "fr", "es", "zh-CN", "ru", "pt", "ko"],
defaultLocale: "en",
urlMappingStrategy: "rewrite",
});
diff --git a/prisma/migrations/20250630041651_add_korean_i18n_fields/migration.sql b/prisma/migrations/20250630041651_add_korean_i18n_fields/migration.sql
new file mode 100644
index 00000000..a98e89be
--- /dev/null
+++ b/prisma/migrations/20250630041651_add_korean_i18n_fields/migration.sql
@@ -0,0 +1,34 @@
+/*
+ Warnings:
+
+ - A unique constraint covering the columns `[slugKo]` on the table `programs` will be added. If there are existing duplicate values, this will fail.
+ - Added the required column `instructionsKo` to the `program_session_exercises` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `descriptionKo` to the `program_sessions` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `slugKo` to the `program_sessions` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `titleKo` to the `program_sessions` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `descriptionKo` to the `program_weeks` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `titleKo` to the `program_weeks` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `descriptionKo` to the `programs` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `slugKo` to the `programs` table without a default value. This is not possible if the table is not empty.
+ - Added the required column `titleKo` to the `programs` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "program_session_exercises" ADD COLUMN "instructionsKo" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "program_sessions" ADD COLUMN "descriptionKo" TEXT NOT NULL,
+ADD COLUMN "slugKo" TEXT NOT NULL,
+ADD COLUMN "titleKo" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "program_weeks" ADD COLUMN "descriptionKo" TEXT NOT NULL,
+ADD COLUMN "titleKo" TEXT NOT NULL;
+
+-- AlterTable
+ALTER TABLE "programs" ADD COLUMN "descriptionKo" TEXT NOT NULL,
+ADD COLUMN "slugKo" TEXT NOT NULL,
+ADD COLUMN "titleKo" TEXT NOT NULL;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "programs_slugKo_key" ON "programs"("slugKo");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index ca2b884d..3af727d7 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -484,18 +484,21 @@ model Program {
slugPt String @unique
slugRu String @unique
slugZhCn String @unique
+ slugKo String @unique
title String
titleEn String
titleEs String
titlePt String
titleRu String
titleZhCn String
+ titleKo String
description String @db.Text
descriptionEn String @db.Text
descriptionEs String @db.Text
descriptionPt String @db.Text
descriptionRu String @db.Text
descriptionZhCn String @db.Text
+ descriptionKo String @db.Text
category String
image String
level ProgramLevel
@@ -542,12 +545,14 @@ model ProgramWeek {
titlePt String
titleRu String
titleZhCn String
+ titleKo String
description String @db.Text
descriptionEn String @db.Text
descriptionEs String @db.Text
descriptionPt String @db.Text
descriptionRu String @db.Text
descriptionZhCn String @db.Text
+ descriptionKo String @db.Text
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
sessions ProgramSession[]
@@ -566,18 +571,21 @@ model ProgramSession {
titlePt String
titleRu String
titleZhCn String
+ titleKo String
slug String
slugEn String
slugEs String
slugPt String
slugRu String
slugZhCn String
+ slugKo String
description String @db.Text
descriptionEn String @db.Text
descriptionEs String @db.Text
descriptionPt String @db.Text
descriptionRu String @db.Text
descriptionZhCn String @db.Text
+ descriptionKo String @db.Text
equipment ExerciseAttributeValueEnum[] @default([])
estimatedMinutes Int
isPremium Boolean @default(true)
@@ -607,6 +615,7 @@ model ProgramSessionExercise {
instructionsPt String @db.Text
instructionsRu String @db.Text
instructionsZhCn String @db.Text
+ instructionsKo String @db.Text
session ProgramSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
exercise Exercise @relation(fields: [exerciseId], references: [id])
diff --git a/src/components/seo/SEOHead.tsx b/src/components/seo/SEOHead.tsx
index 3964127d..27b920a5 100644
--- a/src/components/seo/SEOHead.tsx
+++ b/src/components/seo/SEOHead.tsx
@@ -78,6 +78,7 @@ export function generateSEOMetadata({
"pt-PT": `${baseUrl}/pt`,
"ru-RU": `${baseUrl}/ru`,
"zh-CN": `${baseUrl}/zh-CN`,
+ "ko-KR": `${baseUrl}/ko`,
"x-default": baseUrl,
},
},
@@ -86,15 +87,59 @@ export function generateSEOMetadata({
description: finalDescription,
url: finalCanonical,
siteName: SiteConfig.title,
- locale: locale === "en" ? "en_US" : locale === "es" ? "es_ES" : locale === "pt" ? "pt_PT" : locale === "ru" ? "ru_RU" : locale === "zh-CN" ? "zh_CN" : "fr_FR",
+ locale:
+ locale === "en"
+ ? "en_US"
+ : locale === "es"
+ ? "es_ES"
+ : locale === "pt"
+ ? "pt_PT"
+ : locale === "ru"
+ ? "ru_RU"
+ : locale === "zh-CN"
+ ? "zh_CN"
+ : locale === "ko"
+ ? "ko_KR"
+ : "fr_FR",
alternateLocale: [
- "fr_FR", "fr_CA", "fr_CH", "fr_BE",
- "en_US", "en_GB", "en_CA", "en_AU",
- "es_ES", "es_MX", "es_AR", "es_CL",
- "pt_PT", "pt_BR",
- "ru_RU", "ru_BY", "ru_KZ",
- "zh_CN", "zh_TW", "zh_HK"
- ].filter(alt => alt !== (locale === "en" ? "en_US" : locale === "es" ? "es_ES" : locale === "pt" ? "pt_PT" : locale === "ru" ? "ru_RU" : locale === "zh-CN" ? "zh_CN" : "fr_FR")),
+ "fr_FR",
+ "fr_CA",
+ "fr_CH",
+ "fr_BE",
+ "en_US",
+ "en_GB",
+ "en_CA",
+ "en_AU",
+ "es_ES",
+ "es_MX",
+ "es_AR",
+ "es_CL",
+ "pt_PT",
+ "pt_BR",
+ "ru_RU",
+ "ru_BY",
+ "ru_KZ",
+ "zh_CN",
+ "zh_TW",
+ "zh_HK",
+ "ko_KR",
+ ].filter(
+ (alt) =>
+ alt !==
+ (locale === "en"
+ ? "en_US"
+ : locale === "es"
+ ? "es_ES"
+ : locale === "pt"
+ ? "pt_PT"
+ : locale === "ru"
+ ? "ru_RU"
+ : locale === "zh-CN"
+ ? "zh_CN"
+ : locale === "ko"
+ ? "ko_KR"
+ : "fr_FR"),
+ ),
images: [
{
url: finalOgImage,
diff --git a/src/components/seo/duration-badge.tsx b/src/components/seo/duration-badge.tsx
index 4ef90bb4..5845e13e 100644
--- a/src/components/seo/duration-badge.tsx
+++ b/src/components/seo/duration-badge.tsx
@@ -8,13 +8,7 @@ interface DurationBadgeProps {
className?: string;
}
-export function DurationBadge({
- durationWeeks,
- sessionsPerWeek,
- sessionDurationMin,
- locale,
- className = ""
-}: DurationBadgeProps) {
+export function DurationBadge({ durationWeeks, sessionsPerWeek, sessionDurationMin, locale, className = "" }: DurationBadgeProps) {
const totalMinutes = durationWeeks * sessionsPerWeek * sessionDurationMin;
const totalHours = Math.round(totalMinutes / 60);
@@ -29,6 +23,8 @@ export function DurationBadge({
return `${durationWeeks} недель • ${totalHours}ч всего`;
} else if (locale === "zh-CN") {
return `${durationWeeks} 周 • 总共${totalHours}小时`;
+ } else if (locale === "ko") {
+ return `${durationWeeks}주 • 총 ${totalHours}시간`;
} else {
return `${durationWeeks} semaines • ${totalHours}h total`;
}
@@ -38,7 +34,7 @@ export function DurationBadge({
{formatDuration()}
-
+
{/* Hidden structured data for SEO */}
PT{totalMinutes}M
@@ -46,4 +42,4 @@ export function DurationBadge({
diff --git a/src/features/admin/programs/ui/create-program-form.tsx b/src/features/admin/programs/ui/create-program-form.tsx
index d39524fe..8dccf416 100644
--- a/src/features/admin/programs/ui/create-program-form.tsx
+++ b/src/features/admin/programs/ui/create-program-form.tsx
@@ -17,12 +17,14 @@ const programSchema = z.object({
titlePt: z.string().min(1, "Le titre en portugais est requis"),
titleRu: z.string().min(1, "Le titre en russe est requis"),
titleZhCn: z.string().min(1, "Le titre en chinois est requis"),
+ titleKo: z.string().min(1, "Le titre en cooréen est requis"),
description: z.string().min(1, "La description est requise"),
descriptionEn: z.string().min(1, "La description en anglais est requise"),
descriptionEs: z.string().min(1, "La description en espagnol est requise"),
descriptionPt: z.string().min(1, "La description en portugais est requise"),
descriptionRu: z.string().min(1, "La description en russe est requise"),
descriptionZhCn: z.string().min(1, "La description en chinois est requise"),
+ descriptionKo: z.string().min(1, "La description en cooréen est requise"),
category: z.string().min(1, "La catégorie est requise"),
image: z.string().url("URL d'image invalide"),
level: z.nativeEnum(ProgramLevel),
@@ -100,12 +102,14 @@ export function CreateProgramForm({ currentStep, onStepComplete, onSuccess, onCa
titlePt: "",
titleRu: "",
titleZhCn: "",
+ titleKo: "",
description: "",
descriptionEn: "",
descriptionEs: "",
descriptionPt: "",
descriptionRu: "",
descriptionZhCn: "",
+ descriptionKo: "",
},
});
@@ -152,7 +156,7 @@ export function CreateProgramForm({ currentStep, onStepComplete, onSuccess, onCa
diff --git a/src/features/admin/programs/ui/edit-session-modal.tsx b/src/features/admin/programs/ui/edit-session-modal.tsx
index 872270cf..8c12c3b1 100644
--- a/src/features/admin/programs/ui/edit-session-modal.tsx
+++ b/src/features/admin/programs/ui/edit-session-modal.tsx
@@ -25,12 +25,14 @@ const sessionSchema = z.object({
titlePt: z.string().min(1, "Le titre en portugais est requis"),
titleRu: z.string().min(1, "Le titre en russe est requis"),
titleZhCn: z.string().min(1, "Le titre en chinois est requis"),
+ titleKo: z.string().min(1, "Le titre en coréen est requis"),
description: z.string().min(1, "La description est requise"),
descriptionEn: z.string().min(1, "La description en anglais est requise"),
descriptionEs: z.string().min(1, "La description en espagnol est requise"),
descriptionPt: z.string().min(1, "La description en portugais est requise"),
descriptionRu: z.string().min(1, "La description en russe est requise"),
descriptionZhCn: z.string().min(1, "La description en chinois est requise"),
+ descriptionKo: z.string().min(1, "La description en coréen est requise"),
estimatedMinutes: z.number().min(5, "Au moins 5 minutes"),
isPremium: z.boolean(),
equipment: z.array(z.nativeEnum(ExerciseAttributeValueEnum)),
@@ -75,12 +77,14 @@ export function EditSessionModal({ open, onOpenChange, session }: EditSessionMod
titlePt: session.titlePt,
titleRu: session.titleRu,
titleZhCn: session.titleZhCn,
+ titleKo: session.titleKo,
description: session.description,
descriptionEn: session.descriptionEn,
descriptionEs: session.descriptionEs,
descriptionPt: session.descriptionPt,
descriptionRu: session.descriptionRu,
descriptionZhCn: session.descriptionZhCn,
+ descriptionKo: session.descriptionKo,
estimatedMinutes: session.estimatedMinutes,
isPremium: session.isPremium,
equipment: session.equipment,
@@ -107,6 +111,7 @@ export function EditSessionModal({ open, onOpenChange, session }: EditSessionMod
titlePt: data.titlePt,
titleRu: data.titleRu,
titleZhCn: data.titleZhCn,
+ titleKo: data.titleKo,
});
await updateSession({
@@ -160,6 +165,9 @@ export function EditSessionModal({ open, onOpenChange, session }: EditSessionMod
setActiveTab("zh")} type="button">
🇨🇳 ZH
+ setActiveTab("ko")} type="button">
+ 🇰🇷 KO
+
{/* French Fields */}
@@ -258,6 +266,22 @@ export function EditSessionModal({ open, onOpenChange, session }: EditSessionMod