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({
); -} \ No newline at end of file +} diff --git a/src/features/admin/programs/actions/add-exercise.action.ts b/src/features/admin/programs/actions/add-exercise.action.ts index 18db6e6a..eef589ec 100644 --- a/src/features/admin/programs/actions/add-exercise.action.ts +++ b/src/features/admin/programs/actions/add-exercise.action.ts @@ -61,6 +61,7 @@ export async function addExerciseToSession(data: AddExerciseData) { instructionsPt: data.instructionsEn, instructionsRu: data.instructionsEn, instructionsZhCn: data.instructionsEn, + instructionsKo: data.instructionsEn, suggestedSets: { create: data.suggestedSets.map((set) => ({ setIndex: set.setIndex, diff --git a/src/features/admin/programs/actions/add-session.action.ts b/src/features/admin/programs/actions/add-session.action.ts index 86d74de7..fee16ff8 100644 --- a/src/features/admin/programs/actions/add-session.action.ts +++ b/src/features/admin/programs/actions/add-session.action.ts @@ -16,18 +16,21 @@ interface AddSessionData { titlePt: string; titleRu: string; titleZhCn: string; + titleKo: string; slug: string; slugEn: string; slugEs: string; slugPt: string; slugRu: string; slugZhCn: string; + slugKo: string; description: string; descriptionEn: string; descriptionEs: string; descriptionPt: string; descriptionRu: string; descriptionZhCn: string; + descriptionKo: string; equipment: ExerciseAttributeValueEnum[]; estimatedMinutes: number; isPremium: boolean; @@ -67,18 +70,21 @@ export async function addSessionToWeek(data: AddSessionData) { titlePt: data.titlePt, titleRu: data.titleRu, titleZhCn: data.titleZhCn, + titleKo: data.titleKo, slug: data.slug, slugEn: data.slugEn, slugEs: data.slugEs, slugPt: data.slugPt, slugRu: data.slugRu, slugZhCn: data.slugZhCn, + slugKo: data.slugKo, description: data.description, descriptionEn: data.descriptionEn, descriptionEs: data.descriptionEs, descriptionPt: data.descriptionPt, descriptionRu: data.descriptionRu, descriptionZhCn: data.descriptionZhCn, + descriptionKo: data.descriptionKo, equipment: data.equipment, estimatedMinutes: data.estimatedMinutes, isPremium: data.isPremium, diff --git a/src/features/admin/programs/actions/add-week.action.ts b/src/features/admin/programs/actions/add-week.action.ts index 1001bd2f..918620d9 100644 --- a/src/features/admin/programs/actions/add-week.action.ts +++ b/src/features/admin/programs/actions/add-week.action.ts @@ -16,12 +16,14 @@ interface AddWeekData { titlePt: string; titleRu: string; titleZhCn: string; + titleKo: string; description?: string; descriptionEn?: string; descriptionEs?: string; descriptionPt?: string; descriptionRu?: string; descriptionZhCn?: string; + descriptionKo?: string; } export async function addWeekToProgram(data: AddWeekData) { @@ -58,12 +60,14 @@ export async function addWeekToProgram(data: AddWeekData) { titlePt: data.titlePt, titleRu: data.titleRu, titleZhCn: data.titleZhCn, + titleKo: data.titleKo, description: data.description || "", descriptionEn: data.descriptionEn || "", descriptionEs: data.descriptionEs || "", descriptionPt: data.descriptionPt || "", descriptionRu: data.descriptionRu || "", descriptionZhCn: data.descriptionZhCn || "", + descriptionKo: data.descriptionKo || "", }, }); diff --git a/src/features/admin/programs/actions/create-program.action.ts b/src/features/admin/programs/actions/create-program.action.ts index 2a521347..828c1d7e 100644 --- a/src/features/admin/programs/actions/create-program.action.ts +++ b/src/features/admin/programs/actions/create-program.action.ts @@ -16,12 +16,14 @@ interface CreateProgramData { titlePt: string; titleRu: string; titleZhCn: string; + titleKo: string; description: string; descriptionEn: string; descriptionEs: string; descriptionPt: string; descriptionRu: string; descriptionZhCn: string; + descriptionKo: string; category: string; image: string; level: ProgramLevel; @@ -60,11 +62,12 @@ export async function createProgram(data: CreateProgramData) { const slugPt = generateSlug(data.titlePt); const slugRu = generateSlug(data.titleRu); const slugZhCn = generateSlug(data.titleZhCn); + const slugKo = generateSlug(data.titleKo); // Check if any slug already exists const existingProgram = await prisma.program.findFirst({ where: { - OR: [{ slug }, { slugEn }, { slugEs }, { slugPt }, { slugRu }, { slugZhCn }], + OR: [{ slug }, { slugEn }, { slugEs }, { slugPt }, { slugRu }, { slugZhCn }, { slugKo }], }, }); @@ -80,18 +83,21 @@ export async function createProgram(data: CreateProgramData) { slugPt, slugRu, slugZhCn, + slugKo, title: data.title, titleEn: data.titleEn, titleEs: data.titleEs, titlePt: data.titlePt, titleRu: data.titleRu, titleZhCn: data.titleZhCn, + titleKo: data.titleKo, description: data.description, descriptionEn: data.descriptionEn, descriptionEs: data.descriptionEs, descriptionPt: data.descriptionPt, descriptionRu: data.descriptionRu, descriptionZhCn: data.descriptionZhCn, + descriptionKo: data.descriptionKo, category: data.category, image: data.image, level: data.level, diff --git a/src/features/admin/programs/actions/update-exercise-sets.action.ts b/src/features/admin/programs/actions/update-exercise-sets.action.ts index db232fc8..31198d26 100644 --- a/src/features/admin/programs/actions/update-exercise-sets.action.ts +++ b/src/features/admin/programs/actions/update-exercise-sets.action.ts @@ -54,10 +54,10 @@ export async function updateExerciseSets(exerciseId: string, sets: SetData[]) { // Revalider les caches revalidatePath("/admin/programs"); - + return { success: true }; } catch (error) { console.error("Error updating exercise sets:", error); throw new Error("Erreur lors de la mise à jour des séries"); } -} \ No newline at end of file +} diff --git a/src/features/admin/programs/actions/update-program.action.ts b/src/features/admin/programs/actions/update-program.action.ts index 7c996336..842818e0 100644 --- a/src/features/admin/programs/actions/update-program.action.ts +++ b/src/features/admin/programs/actions/update-program.action.ts @@ -15,12 +15,14 @@ interface UpdateProgramData { titlePt: string; titleRu: string; titleZhCn: string; + titleKo: string; description: string; descriptionEn: string; descriptionEs: string; descriptionPt: string; descriptionRu: string; descriptionZhCn: string; + descriptionKo: string; category: string; image: string; level: ProgramLevel; @@ -56,6 +58,7 @@ export async function updateProgram(programId: string, data: UpdateProgramData) const slugPt = generateSlug(data.titlePt); const slugRu = generateSlug(data.titleRu); const slugZhCn = generateSlug(data.titleZhCn); + const slugKo = generateSlug(data.titleKo); // Check if any slug already exists (excluding current program) const existingProgram = await prisma.program.findFirst({ @@ -63,7 +66,7 @@ export async function updateProgram(programId: string, data: UpdateProgramData) AND: [ { id: { not: programId } }, { - OR: [{ slug }, { slugEn }, { slugEs }, { slugPt }, { slugRu }, { slugZhCn }], + OR: [{ slug }, { slugEn }, { slugEs }, { slugPt }, { slugRu }, { slugZhCn }, { slugKo }], }, ], }, @@ -86,18 +89,21 @@ export async function updateProgram(programId: string, data: UpdateProgramData) slugPt, slugRu, slugZhCn, + slugKo, title: data.title, titleEn: data.titleEn, titleEs: data.titleEs, titlePt: data.titlePt, titleRu: data.titleRu, titleZhCn: data.titleZhCn, + titleKo: data.titleKo, description: data.description, descriptionEn: data.descriptionEn, descriptionEs: data.descriptionEs, descriptionPt: data.descriptionPt, descriptionRu: data.descriptionRu, descriptionZhCn: data.descriptionZhCn, + descriptionKo: data.descriptionKo, category: data.category, image: data.image, level: data.level, diff --git a/src/features/admin/programs/actions/update-session.action.ts b/src/features/admin/programs/actions/update-session.action.ts index fd7ab01e..e3e72f56 100644 --- a/src/features/admin/programs/actions/update-session.action.ts +++ b/src/features/admin/programs/actions/update-session.action.ts @@ -15,18 +15,21 @@ interface UpdateSessionData { titlePt: string; titleRu: string; titleZhCn: string; + titleKo: string; slug: string; slugEn: string; slugEs: string; slugPt: string; slugRu: string; slugZhCn: string; + slugKo: string; description: string; descriptionEn: string; descriptionEs: string; descriptionPt: string; descriptionRu: string; descriptionZhCn: string; + descriptionKo: string; equipment: ExerciseAttributeValueEnum[]; estimatedMinutes: number; isPremium: boolean; @@ -83,6 +86,7 @@ export async function updateSession(data: UpdateSessionData) { slugPt: await ensureUniqueSessionSlug(data.slugPt, "slugPt"), slugRu: await ensureUniqueSessionSlug(data.slugRu, "slugRu"), slugZhCn: await ensureUniqueSessionSlug(data.slugZhCn, "slugZhCn"), + slugKo: await ensureUniqueSessionSlug(data.slugKo, "slugKo"), }; const updatedSession = await prisma.programSession.update({ @@ -94,18 +98,21 @@ export async function updateSession(data: UpdateSessionData) { titlePt: data.titlePt, titleRu: data.titleRu, titleZhCn: data.titleZhCn, + titleKo: data.titleKo, slug: uniqueSlugs.slug, slugEn: uniqueSlugs.slugEn, slugEs: uniqueSlugs.slugEs, slugPt: uniqueSlugs.slugPt, slugRu: uniqueSlugs.slugRu, slugZhCn: uniqueSlugs.slugZhCn, + slugKo: uniqueSlugs.slugKo, description: data.description, descriptionEn: data.descriptionEn, descriptionEs: data.descriptionEs, descriptionPt: data.descriptionPt, descriptionRu: data.descriptionRu, descriptionZhCn: data.descriptionZhCn, + descriptionKo: data.descriptionKo, equipment: data.equipment, estimatedMinutes: data.estimatedMinutes, isPremium: data.isPremium, diff --git a/src/features/admin/programs/actions/update-week.action.ts b/src/features/admin/programs/actions/update-week.action.ts index e6dd755e..ef48369d 100644 --- a/src/features/admin/programs/actions/update-week.action.ts +++ b/src/features/admin/programs/actions/update-week.action.ts @@ -14,12 +14,14 @@ interface UpdateWeekData { titlePt: string; titleRu: string; titleZhCn: string; + titleKo: string; description?: string; descriptionEn?: string; descriptionEs?: string; descriptionPt?: string; descriptionRu?: string; descriptionZhCn?: string; + descriptionKo?: string; } export async function updateWeek(weekId: string, data: UpdateWeekData) { @@ -43,12 +45,14 @@ export async function updateWeek(weekId: string, data: UpdateWeekData) { titlePt: data.titlePt, titleRu: data.titleRu, titleZhCn: data.titleZhCn, + titleKo: data.titleKo, description: data.description || "", descriptionEn: data.descriptionEn || "", descriptionEs: data.descriptionEs || "", descriptionPt: data.descriptionPt || "", descriptionRu: data.descriptionRu || "", descriptionZhCn: data.descriptionZhCn || "", + descriptionKo: data.descriptionKo || "", }, }); diff --git a/src/features/admin/programs/ui/add-session-modal.tsx b/src/features/admin/programs/ui/add-session-modal.tsx index 6e076382..bbeb1820 100644 --- a/src/features/admin/programs/ui/add-session-modal.tsx +++ b/src/features/admin/programs/ui/add-session-modal.tsx @@ -24,12 +24,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 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"), estimatedMinutes: z.number().min(5, "Au moins 5 minutes"), isPremium: z.boolean(), equipment: z.array(z.nativeEnum(ExerciseAttributeValueEnum)), @@ -74,12 +76,14 @@ export function AddSessionModal({ open, onOpenChange, weekId, nextSessionNumber titlePt: `Sessão ${nextSessionNumber}`, titleRu: `Сессия ${nextSessionNumber}`, titleZhCn: `第${nextSessionNumber}节`, + titleKo: `세션 ${nextSessionNumber}`, description: `Description de la séance ${nextSessionNumber}`, descriptionEn: `Description of session ${nextSessionNumber}`, descriptionEs: `Descripción de la sesión ${nextSessionNumber}`, descriptionPt: `Descrição da sessão ${nextSessionNumber}`, descriptionRu: `Описание сессии ${nextSessionNumber}`, descriptionZhCn: `第${nextSessionNumber}节课程描述`, + descriptionKo: `${nextSessionNumber} 세션의 설명`, estimatedMinutes: 30, isPremium: true, equipment: [], @@ -106,6 +110,7 @@ export function AddSessionModal({ open, onOpenChange, weekId, nextSessionNumber titlePt: data.titlePt, titleRu: data.titleRu, titleZhCn: data.titleZhCn, + titleKo: data.titleKo, }); await addSessionToWeek({ @@ -162,6 +167,9 @@ export function AddSessionModal({ open, onOpenChange, weekId, nextSessionNumber + {/* French Fields */} @@ -260,6 +268,22 @@ export function AddSessionModal({ open, onOpenChange, weekId, nextSessionNumber )} + {/* Korean Fields */} + {activeTab === "ko" && ( +
+
+ + + {errors.titleKo &&

{errors.titleKo.message}

} +
+
+ +