1import { buildLocalizedUrl, getLocale as getRouteLocale } from './app-mode.js?v=485'; 2 3const DEFAULT_LOCALE = 'ko'; 4const ENGLISH_LOCALE = 'en'; 5 6const MESSAGES = { 7 ko: { 8 'common.save': 'ì ì¥í기', 9 'common.logout': 'ë¡ê·¸ìì', 10 'common.loading': 'íì¸ ì¤...', 11 'auth.loginRequired': 'ë¡ê·¸ì¸ì´ íìí©ëë¤.', 12 'auth.startWithGoogle': 'Googleë¡ ììí기', 13 'profile.languageKorean': 'íêµì´', 14 'profile.languageEnglish': 'English', 15 'notification.enable': 'ì림 ì¼ê¸°', 16 'toast.aiFailed': 'AI ë¶ìì ì¤í¨íìµëë¤. ë¤ì ìëí´ ì£¼ì¸ì.', 17 'toast.analysisFailed': 'ë¶ì ì¤ ì¤ë¥ê° ë°ìíìµëë¤.', 18 'toast.notHealthScreenshot': 'ê±´ê°/ë§ë³´ê¸° ì± ìº¡ì²ê° ìë ê² ê°ìµëë¤.', 19 'analysis.signedOut': 'ë¡ê·¸ì¸ì´ íë ¸ì´ì. ë¤ì ë¡ê·¸ì¸í ë¤ ìëí´ ì£¼ì¸ì.', 20 'analysis.tooSlow': 'ë¶ìì´ ì¤ë ê±¸ë¦¬ê³ ìì´ì. ì ì í ë¤ì ìëí´ ì£¼ì¸ì.', 21 'analysis.busy': 'ìì²ì´ ëª°ë ¤ ìì´ì. ì ì í ë¤ì ìëí´ ì£¼ì¸ì.', 22 'analysis.offline': 'ì°ê²°ì´ ëê²¼ì´ì. ë¤í¸ìí¬ë¥¼ íì¸í ë¤ ë¤ì ìëí´ ì£¼ì¸ì.', 23 'kst.badge': 'KST', 24 'kst.badgeAria': 'ë ì§ ê¸°ì¤ ìë´ (íêµìê°)', 25 'kst.title': 'ë ì§ ê¸°ì¤: íêµìê°(KST)', 26 'kst.body': 'í´ë¹ì¤ì¿¨ì í루ë íêµìê°(KST) 기ì¤ì´ìì.\nì§ê¸ íêµì {now}ì ëë¤.\n\në³´ì íëë íêµìê° ì¤ì 9ìì ì´ê¸°íë¼ì.\nì¬ì§ì ì´¬ì ìê°ë íêµìê°ì¼ë¡ íì°í´ ì¸ì íë, íì§ ë ì§ì ë¬ë¼ ë³´ì¬ë ê·¸ëë¡ ì¬ë¦¬ìë©´ ë©ëë¤.', 27 // 첫 ê°ì ëì íë©´ìì ëê°ë ë²í¼ â "ë¡ê·¸ìì"ê³¼ ë¤ë¥¸ ëìì¤ 28 'reconsent.signup.later': 'ê·¸ë§ë기' 29 }, 30 en: { 31 'common.save': 'Save', 32 'common.logout': 'Log out', 33 'common.loading': 'Checking...', 34 'auth.loginRequired': 'Please sign in first.', 35 'auth.startWithGoogle': 'Start with Google', 36 'profile.languageKorean': 'íêµì´', 37 'profile.languageEnglish': 'English', 38 'notification.enable': 'Turn on notifications', 39 'toast.aiFailed': 'AI analysis failed. Please try again.', 40 'toast.analysisFailed': 'Something went wrong during analysis.', 41 'toast.notHealthScreenshot': 'This does not look like a health or step-count screenshot.', 42 'analysis.signedOut': 'Your session expired. Sign in again and retry.', 43 'analysis.tooSlow': 'The analysis is taking too long. Please try again shortly.', 44 'analysis.busy': 'Too many requests right now. Please try again shortly.', 45 'analysis.offline': 'Connection lost. Check your network and try again.', 46 'kst.badge': 'KST', 47 'kst.badgeAria': 'About the date basis (Korea time)', 48 'kst.title': 'Dates follow Korea time (KST)', 49 'kst.body': 'A day in Habit School runs on Korea Standard Time (KST, UTC+9).\nIt is now {now} in Korea.\n\nDaily reward limits reset at 9:00 AM KST.\nPhoto capture times are converted to Korea time as well, so go ahead and upload even if the date looks different from your local one.', 50 'diet.grade.A': 'Excellent', 51 'diet.grade.B': 'Good', 52 'diet.grade.C': 'Average', 53 'diet.grade.D': 'Needs work', 54 'diet.grade.F': 'High risk', 55 'diet.category.natural': 'Whole food', 56 'diet.category.processed': 'Minimally processed', 57 'diet.category.ultraprocessed': 'Ultra-processed', 58 'diet.category.other': 'Other', 59 'diet.score.vitamins': 'Vitamins', 60 'diet.score.minerals': 'Minerals', 61 'diet.score.fiber': 'Fiber', 62 'diet.score.antioxidants': 'Antioxidants', 63 'diet.naturalRatio': 'Whole-food ratio', 64 'diet.micronutrientScore': 'Micronutrient score', 65 'diet.insulinImpact': 'Likely insulin impact', 66 'diet.daySummary.wholeFoods': 'Whole foods {value}%', 67 'diet.daySummary.ultra': '{count} ultra-processed item(s)', 68 'diet.daySummary.noUltra': 'No ultra-processed items', 69 'exercise.intensity.low': 'Light', 70 'exercise.intensity.moderate': 'Moderate', 71 'exercise.intensity.high': 'Hard', 72 'exercise.intensity.veryHigh': 'Very hard', 73 'exercise.progress.complete': 'Complete!', 74 'exercise.progress.almost': 'Almost there', 75 'exercise.progress.half': 'Halfway', 76 'exercise.progress.keepGoing': 'Keep going', 77 'exercise.type': 'Detected', 78 'exercise.intensityTitle': 'Exercise intensity analysis', 79 'exercise.dailyProgress': 'Daily recommendation progress', 80 'exercise.coachFeedback': 'AI trainer feedback', 81 'mind.sleepAnalysis': 'Sleep analysis', 82 'mind.mindAnalysis': 'Mind analysis', 83 'mind.sleepDuration': 'Sleep duration', 84 'mind.sleepQuality': 'Sleep quality', 85 'mind.emotionTone': 'Emotional tone', 86 'mind.stressLevel': 'Stress', 87 'mind.coachFeedback': 'AI coach feedback', 88 'voice.unsupported': 'Voice input is not supported in this browser.', 89 'voice.idle': 'Voice input', 90 'voice.starting': 'Listening...', 91 'voice.listening': 'Listening...', 92 'voice.startStatus': 'Listening. Speak your gratitude journal now.', 93 'voice.resultStatus': 'Adding what you said to the journal.', 94 'voice.retry': 'Voice input failed. Please try again.', 95 'voice.permission': 'Allow microphone permission to use voice input.', 96 'voice.noSpeech': 'I could not hear anything. Please try again.', 97 'voice.noMic': 'Could not find a microphone.', 98 'voice.network': 'Please check your network connection.', 99 'voice.done': 'Voice input complete.', 100 'voice.tryAgain': 'I could not hear anything. Please try again.', 101 'voice.startingMic': 'Opening the microphone...', 102 'profile.notificationGranted': 'Notifications are on for this device.', 103 'profile.notificationDefault': 'Turn on reminders for check-ins and progress nudges.', 104 'profile.notificationDenied': 'Notifications are blocked in your browser settings.', 105 'profile.notificationUnsupported': 'This browser does not support push notifications.', 106 'profile.notificationInstallRequired': 'Install Habit School to your home screen first on iPhone/iPad.', 107 'disclaimer.medical': 'Habit School helps you build healthy lifestyle habits and does not provide medical advice, diagnosis, or treatment. Its diet analysis, health scores, and other features are for reference only and are not a substitute for professional medical judgment. Always consult a qualified healthcare professional about your health, symptoms, or any diet or exercise plan.', 108 // ìë¨ ë°©ë² ì´ë¦Â·ì¤ëª . í¤ìë ë´ë¶ ID를 ê·¸ëë¡ ì´ë¤(íìëª ê³¼ ë¶ë¦¬). 109 // í¤ê° ìì¼ë©´ t()ê° íêµì´ ì문ì¼ë¡ í´ë°±íë¤. 110 'diet.method.brown_rice_green_veggies.name': 'Brown Rice & Greens', 111 'diet.method.brown_rice_green_veggies.summary': 'Whole grains and vegetables as your base', 112 'diet.method.mediterranean.name': 'Mediterranean', 113 'diet.method.mediterranean.summary': 'Olive oil and fish for heart health',
114 'diet.method.low_carb.name': 'Low-Carb High-Protein', 115 'diet.method.low_carb.summary': 'Less rice and bread, more protein on your plate', 116 'diet.method.intermittent_fasting.name': '16:8 Intermittent Fasting', 117 'diet.method.intermittent_fasting.summary': 'Eat within an 8-hour window and keep the rest of the day clear', 118 'diet.method.switch_on.name': 'Two Weeks Without Ultra-Processed Food', 119 'diet.method.switch_on.summary': 'Cut snacks, processed meat and sweet drinks; fill up on protein and vegetables', 120 'diet.method.none.dashboardTip': 'Your guide and reminders will change.', 121 'diet.method.brown_rice_green_veggies.exerciseSupportTip': 'Start with easy walks.', 122 'diet.method.brown_rice_green_veggies.mindSleepSupportTip': 'Just cutting late-night snacks helps.', 123 'diet.method.mediterranean.exerciseSupportTip': 'Pairs well with short walks.', 124 'diet.method.mediterranean.mindSleepSupportTip': 'Goes well with regular sleep.', 125 'diet.method.low_carb.exerciseSupportTip': 'Keep movement light while you adjust.', 126 'diet.method.low_carb.mindSleepSupportTip': 'Avoid restricting too hard.', 127 'diet.method.intermittent_fasting.exerciseSupportTip': 'Move gently during fasting hours.', 128 'diet.method.intermittent_fasting.mindSleepSupportTip': 'Try to keep a steady sleep schedule.', 129 'diet.method.switch_on.exerciseSupportTip': 'Start light in the first days.', 130 'diet.method.switch_on.mindSleepSupportTip': 'Keep your daily rhythm steady first.', 131 // ìë¨ í ìë´ ì¹´ë(js/diet-program.js) 132 'dietGuide.badge': 'Photos {count}/4', 133 'dietGuide.helper.empty': 'You can save with just one meal photo.', 134 'dietGuide.status.empty': 'Add your first meal photo to start todayâs food log.', 135 'dietGuide.status.partial': '{count} meal photo(s) ready. Add more for up to 30P.', 136 'dietGuide.helper.partialWithMetrics': '{count} meal photo(s) and your fasting metrics are ready to save.', 137 'dietGuide.helper.partial': '{count} meal photo(s) ready to save now.', 138 'dietGuide.status.metricsOnly': 'Fasting metrics entered. Add meal photos to save them together.', 139 'dietGuide.helper.metricsOnly': 'You can save your fasting metrics now.', 140 'dietGuide.status.full': 'All meal slots are filled. Save to add todayâs food points.', 141 'dietGuide.helper.full': 'Food log ready. Save to apply.', 142 'dietGuide.fasting.label': 'Fasting', 143 'dietGuide.fasting.status': 'You are in your fasting hours.', 144 'dietGuide.window': 'Your eating window is {start}â{end}.', 145 'dietGuide.eating.label': 'Eating', 146 'dietGuide.eating.status': 'You are in your eating window.', 147 'dietGuide.finishBy': 'Try to finish by {end}.', 148 'dietGuide.closing.label': 'Closing soon', 149 'dietGuide.closing.status': 'Your eating window is about to close.', 150 'dietGuide.closed.status': 'Todayâs eating window is over.', 151 'dietGuide.closed.helper': 'You can keep logging.', 152 'dietGuide.preset.helper': 'Log whenever you like.', 153 'dietGuide.helper.metricsFirst': 'You can save just your fasting metrics first.', 154 'dietProgram.noReminders': 'No reminders', 155 'dietProgram.chooseMethod': 'Choose a food method', 156 'dietProgram.remindersOff': 'Meal-time reminders are off.', 157 'dietProgram.analysisTip': 'Food tip · {guide}', 158 'dietProgram.toggle.needMethod': 'Choose a food method first to turn on reminders.', 159 'dietProgram.toggle.deviceOff': 'Meal-time reminders are on, but notifications are off on this device.', 160 'dietProgram.toggle.on': 'This device gets reminders matched to your food method.', 161 'dietProgram.toggle.off': 'Keep your food method and turn on reminders whenever you need them.', 162 'diet.method.difficulty.기본': 'Basic', 163 'diet.method.difficulty.ì¬ì': 'Easy', 164 'diet.method.difficulty.ë³´íµ': 'Moderate', 165 'diet.method.difficulty.ëì ': 'Challenging', 166 'diet.window.title': 'My eating window', 167 'diet.window.save': 'Save', 168 'diet.window.needMethod': 'Choose a food method first to set your eating window.', 169 'diet.window.helperFasting': 'Eat only within this window. Pick a start time and the end is set 8 hours later. Reminders: {plan}', 170 'diet.window.helperGeneral': 'We send reminders around these times. Reminders: {plan}', 171 'diet.window.saved': 'â Eating window saved: {start}â{end}.', 172 'diet.window.stepError': 'Please pick times in 30-minute steps.', 173 'diet.window.minError': 'Please set an eating window of at least {hours} hours.', 174 'diet.window.invalid': 'Please check the times again.', 175 'invite.landingTitle': 'ð A friend invited you', 176 'mind.gratitudePlaceholder': 'Write down three things you felt grateful for after meditating.', 177 'login.filmLabel': 'A 30-second story · The Breakfast Table', 178 'login.filmAria': 'Watch the 30-second story "The Breakfast Table"', 179 'login.filmClose': 'Close video', 180 'login.film2Label': "A 45-second story · Dad's Bike", 181 'login.film2Aria': "Watch the 45-second story \"Dad's Bike\"",
182 'login.film3Label': "A 45-second story · Mom's Blood Sugar", 183 'login.film3Aria': "Watch the 45-second story \"Mom's Blood Sugar\"", 184 'login.tagline': 'Doctors handle the emergencies.<br>Staying well is yours to keep.<br><strong>A gentler way to build good habits.</strong>', 185 'consent.all': 'Agree to all', 186 'consent.required': 'Required', 187 'consent.optional': 'Optional', 188 'consent.terms': '<a href="/en/terms" target="_blank" rel="noopener" onclick="openConsentDoc(event, this)">Terms of Service</a>', 189 'consent.privacy': '<a href="/en/privacy" target="_blank" rel="noopener" onclick="openConsentDoc(event, this)">Collection and use of personal data</a>', 190 'consent.age': 'I am 14 years of age or older', 191 'reconsent.badge': 'Updated terms', 192 'reconsent.title': 'Our Terms and Privacy Policy have changed', 193 'reconsent.copy': 'Please agree to the points below to keep using Habit School.', 194 'reconsent.change1': '<strong>The minimum age is now 14</strong> (previously stated as 18).', 195 'reconsent.change2': '<strong>Redeeming a coupon sends your mobile number</strong> to the delivery agency (KT Alpha).', 196 'reconsent.change3': '<strong>Deleting your account now erases everything</strong>, with nothing kept back.', 197 'reconsent.change4': '<strong>Photos and videos are stored in the United States</strong>; logs and figures stay in Korea.', 198 'reconsent.change5': '<strong>Habit Coach chat</strong> forwards only ! commands â ordinary conversation is never sent.', 199 'reconsent.note': 'You can decline the optional item and carry on. Only the blood test and body composition features are locked.', 200 'reconsent.later': 'Log out', 201 'reconsent.agree': 'Agree and continue', 202 // 첫 ê°ì ëì íë©´ ì ì© â 기존 íìì ì½ê´ ê°ì íë©´ê³¼ ë¤ë¥¸ 문구를 ì´ë¤. 203 'reconsent.signup.badge': 'Before you start', 204 'reconsent.signup.title': 'One quick step before you begin', 205 'reconsent.signup.copy': 'Agree to the required items below to get started.', 206 'reconsent.signup.later': 'Cancel', 207 'consent.sensitive': 'Health data (blood test, body composition)', 208 'consent.note': 'Optional items can be declined. Only blood test and body composition features are locked.', 209 'invite.landingDesc': 'Start here to connect with your friend and get 200P.', 210 'login.point1': 'ð¸ Upload a meal photo and AI analyzes it', 211 'login.point2': 'â Record to earn points and get a coffee', 212 'login.point3': 'ð Start now for a 200P welcome gift', 213 'invite.point1': 'ð¸ Upload a meal photo and AI analyzes it', 214 'invite.point2': 'â Record to earn points and get a coffee', 215 'invite.point3': 'ð¤ Cheer each other on with the friend who invited you', 216 'sharePrompt.kicker': 'Today, wrapped up', 217 'sharePrompt.title': 'Your whole day fits on one card', 218 'sharePrompt.kickerFull': 'Full routine done', 219 'sharePrompt.titleFull': 'You covered food, movement and mind today', 220 'sharePrompt.desc': 'The card carries a join QR code. When a friend joins and records for 3 days, 500P comes back to you.', 221 'sharePrompt.later': 'Later', 222 'sharePrompt.share': 'Share', 223 // í ì¤í¸/confirm â ìì´ ëª¨ëìì ë³´ì´ë 주ì ë©ìì§ 224 'toast.offlineStepsImport': 'You\'re offline â saved as workout photo instead of step count.', 225 'toast.sleepFirstPhotoOnly': 'Only the first photo has been linked to your sleep record.', 226 'toast.sharedDietImported': 'Imported {count} shared photo(s) to diet.', 227 'toast.sharedExerciseImported': 'Shared photo imported as workout.', 228 'toast.sharedBodyImported': 'Body composition values filled in. Check and tap Save.', 229 'toast.sharedSleepImported': 'Shared photo linked to sleep record.', 230 'toast.dietMethodSaveError': 'There was a problem saving the food method. Please try again.', 231 'toast.coachMessageOn': 'Habit Coach messages are on.', 232 'toast.coachMessageOff': 'Habit Coach messages are off.', 233 'toast.coachMessageSaveError': 'Could not save the setting. Please try again.', 234 'toast.loginRequiredShort': 'Please sign in first.', 235 // ì¬ì§ ë ì§ ë¶ì¼ì¹ ë¤ì´ì¼ë¡ê·¸ 236 'photoDate.mismatchAlert': 'â ï¸ The photo date ({date}) does not match the log date ({logDate}). Upload is not allowed.', 237 'photoDate.mismatchConfirm': 'â ï¸ The file date ({date}) does not match the log date ({logDate}). Upload anyway?', 238 // ê±´ê°ì ë³´ ëì ì² í confirm 239 'consent.revokeConfirm': 'Revoke health data consent?\n\nBody composition, medication, and blood test features will be disabled.\nRecords already saved can be deleted separately in your profile.', 240 'toast.sensitiveConsentRevoked': 'Health data consent revoked.', 241 'bloodTest.title': '𩸠Blood test results', 242 'bloodTest.desc': 'Upload your checkup or blood test results as a photo, PDF, or Excel file and AI reads the numbers for you.', 243 'bloodTest.pickPhoto': 'ð¼ï¸ Choose photo', 244 'bloodTest.takePhoto': 'ð· Take photo', 245 'bloodTest.pickFile': 'ð Upload PDF · Excel', 246 'bloodTest.tip': 'ð¡ <strong>Tip:</strong> Make sure the numbers are sharp in the photo. For several pages, upload them one at a time.<br>ð PDFs (up to 15MB) and Excel files (xlsx · xls · csv) from your clinic can be uploaded as they are.<br>ð¥ Checkup reports and hospital lab reports both work.', 247 // í ë²í¼ aria-label 248 'tab.diet': 'Diet', 249 'tab.exercise': 'Exercise', 250 'tab.mind': 'Mind',
251 'tab.dashboard': 'My records', 252 'tab.assets': 'Assets', 253 'tab.gallery': 'Gallery', 254 // 기í UI ë¼ë²¨ 255 'bugReport.ariaLabel': 'Report a bug', 256 'sleepHours.label': 'ð Last night\'s sleep (hours)', 257 'sleepHours.placeholder': 'e.g. 7.5', 258 // ì²´ì±ë¶ ì²´ì¤ê³ ë¸ë¡ (ìë¨ í) 259 'bodyComp.scale.title': '𧬠If you use a body composition scale', 260 'bodyComp.scale.share': 'ð· Share Fitdays result', 261 'bodyComp.scale.library': 'ð¼ï¸ Choose a result photo', 262 'bodyComp.scale.camera': 'ð· Take a photo', 263 'bodyComp.scale.hint': 'In Fitdays, tap Share â Habit School on the results screen to fill in automatically.', 264 'toast.bodyCompNotDetected': 'This doesn\'t look like a body composition results screen. Please take a photo of the scale screen or Fitdays results.', 265 // ë¡ê·¸ì¸ íë©´ Terms/Privacy ë§í¬ 266 'login.terms': 'Terms of Service', 267 'login.privacy': 'Privacy Policy', 268 // íë¡í í Terms/Privacy 269 'profile.terms': 'Terms', 270 'profile.privacy': 'Privacy', 271 'common.cancel': 'Cancel', 272 'common.gotIt': 'Got it', 273 'tab.menu': 'Main menu', 274 'sleepHours.note': 'Used for the sleep part of your healthy habits score. It may al
274so fill in automatically when you upload a screenshot and run AI analysis.', 275 'meditation.methodList': 'Choose a meditation method', 276 // ë¡ê·¸ì¸ íë©´: ì¸ì± ë¸ë¼ì°ì ìë´ 277 'webview.title': 'Google sign-in is limited in in-app browsers', 278 'webview.body': 'Googleâs security policy blocks sign-in inside in-app browsers such as KakaoTalk or Instagram.<br>Please open this page in <strong>Chrome or Safari</strong>.', 279 'webview.openExternal': 'ð Open in browser', 280 'webview.copyLink': 'ð Copy link', 281 // ê³µì ê°ì ¸ì¤ê¸°(ë¤ë¥¸ ì±ìì ê³µì ) 282 'sharedImport.kicker': 'Shared import', 283 'sharedImport.title': 'Where should this photo go?', 284 'sharedImport.diet.title': 'Food', 285 'sharedImport.diet.desc': 'Placed in an empty meal slot', 286 'sharedImport.exercise.title': 'Exercise', 287 'sharedImport.exercise.desc': 'Step screenshot or workout photo', 288 'sharedImport.sleep.title': 'Sleep', 289 'sharedImport.sleep.desc': 'Linked as a sleep screenshot', 290 'sharedImport.body.title': 'Body composition', 291 'sharedImport.body.desc': 'Reads a scale or Fitdays result', 292 // ì¤ë¥ ì ë³´ 293 'bugReport.title': 'ð Report a problem', 294 'bugReport.copy': 'Tell us what happened: which screen you were on, what you tapped, and what went wrong. That helps us fix it fastest.', 295 'bugReport.placeholder': 'e.g. I uploaded a video in the Exercise tab, but it keeps saying the upload is delayed and will not save.', 296 'bugReport.screenshot': 'ð· Screenshot (optional)', 297 'bugReport.note': 'Your device, browser, app version and recent error logs are sent along. Your records and photos are not included.', 298 'bugReport.send': 'Send', 299 // ê³µì ì°½ 300 'share.modalTitle': 'Share â¨', 301 'share.previewTitle': 'Todayâs Habit School check-in ð', 302 'share.previewText': 'Show your friends<br>your healthy habits!', 303 'share.tip': '<strong>On mobile</strong>, system sharing is easiest. <span>On a computer, save the image and copy the caption, then post them in the platform window.</span>', 304 'share.openPlatform': 'Open a platform', 305 'share.platform.kakao': 'KakaoTalk', 306 'share.platform.instagram': 'Instagram', 307 'share.platform.facebook': 'Facebook', 308 'share.platform.blog': 'Blog', 309 'share.quick': 'Quick actions', 310 'share.action.system': 'System share', 311 'share.action.caption': 'Copy caption', 312 'share.action.link': 'Copy link', 313 'share.action.save': 'Save image', 314 // 첫 ë¡ê·¸ì¸: ììí ìµê´ ê³ ë¥´ê¸° 315 'onboarding.kicker': 'Getting started', 316 'onboarding.title': 'Pick one habit to start with', 317 'onboarding.desc': 'Start with one habit today. You can add the others whenever you like.', 318 'onboarding.groupLabel': 'Choose your first habit', 319 'onboarding.diet.title': 'Meal photos', 320 'onboarding.diet.desc': 'Start with one photo', 321 'onboarding.exercise.title': 'Exercise', 322 'onboarding.exercise.desc': 'Steps or workout media', 323 'onboarding.sleep.title': 'Mind', 324 'onboarding.sleep.desc': 'Sleep, meditation, gratitude', 325 'onboarding.gift.title': '200P welcome gift', 326 'onboarding.gift.desc': 'Given on top of your first log points.', 327 'onboarding.start': 'Get started', 328 // 첫 ê¸°ë¡ ê²°ê³¼ 329 'firstRecord.kicker': 'First log done', 330 'firstRecord.title': 'You finished your first step toward this weekâs health goal.', 331 'firstRecord.earned': 'Points for this log', 332 'firstRecord.welcome': 'Welcome gift', 333 'firstRecord.current': 'Current points', 334 'firstRecord.remaining': 'To 2,000P', 335 'firstRecord.finish': 'Done for today', 336 'firstRecord.continue': 'Continue' 337 } 338}; 339 340const ENGLISH_TEXT_REPLACEMENTS = new Map([ 341 ['AI ë¶ì', 'AI analysis'], 342 ['ë¶ì 보기', 'View analysis'], 343 ['ë¶ì ì¤...', 'Analyzing...'], 344 ['ì¬ì§ ì¬ë¦¬ê¸°', 'Upload photo'], 345 ['ìº¡ì² ì¬ë¦¬ê¸°', 'Upload screenshot'], 346 ['ì´ë ì´ë¯¸ì§ ì¬ë¦¬ê¸°', 'Upload workout photo'],
347 ['ì´ë ìì ì¬ë¦¬ê¸°', 'Upload workout video'], 348 ['ì´ë ì´ë¯¸ì§ ë±ë¡', 'Add workout photo'], 349 ['ì´ë ìì ë±ë¡', 'Add workout video'], 350 ['ì´ë ì´ë¯¸ì§', 'Workout photo'], 351 ['ì´ë ìì', 'Workout video'], 352 ['ì¬ì§ì ì´¬ì ë¹ì¼ìë§ ë±ë¡ ê°ë¥í©ëë¤.', 'Photos can only be uploaded on the day they were taken.'], 353 ['ììì ì´¬ì ë¹ì¼ìë§ ë±ë¡ ê°ë¥í©ëë¤.', 'Videos can only be uploaded on the day they were recorded.'], 354 ['ìì ', 'Remove'], 355 ['X ìì ', 'X Remove'], 356 ['미ë¬ì±', 'Not yet'], 357 ['ì ë ¥', 'Enter'], 358 ['ì 기', 'Collapse'], 359 ['í¼ì¹ê¸°', 'Expand'], 360 ['ìì', 'Start'], 361 ['ì¼ìì ì§', 'Pause'], 362 ['ì¬ê°', 'Resume'], 363 ['ì¤ë¨', 'Stop'], 364 ['ìë¨ ì ì¥í기', 'Save food log'], 365 ['ì´ë ì ì¥í기', 'Save exercise log'], 366 ['ë§ì ì ì¥í기', 'Save mind log'], 367 ['ìë¨ ì ì¥íê³ í¬ì¸í¸ ë°ê¸°', 'Save food log'], 368 ['ì´ë ì ì¥íê³ í¬ì¸í¸ ë°ê¸°', 'Save exercise log'], 369 ['ë§ì ì ì¥íê³ í¬ì¸í¸ ë°ê¸°', 'Save mind log'], 370 ['2ì¼ ì´ì ì§ë ë ì§ì ì ì¥í´ë í¬ì¸í¸ë ì¬ë¼ê°ì§ ììµëë¤.', 'Past-date edits are saved, but points are not added.'] 371]); 372 373const SELECTOR_TEXTS = [ 374 ['.skip-to-content', 'Skip to main content'], 375 ['#loginBtn', 'Start with Google'], 376 // ë¡ê·¸ì¸ íë©´ì´ /en ììë ë¨ê² ëë©´ì, ì¬ê¸° íêµì´ê° ê·¸ëë¡ ë³´ìë¤. 377 ['#login-modal .login-title', 'Habit School'], 378 ['#simple-mode-default-btn', 'íêµì´'], 379 ['button[onclick*="openTab(\'diet\')"]', 'ð¥ Food'], 380 ['button[onclick*="openTab(\'exercise\')"]', 'ð Exercise'], 381 ['button[onclick*="openTab(\'sleep\')"]', 'ð§ Mind'], 382 ['button[onclick*="openTab(\'profile\')"]', 'ð¤ Profile'], 383 ['#diet .record-flow-kicker', 'Guide'], 384 ['#diet .simple-mode-record-title', 'Log your food today'], 385 ['#diet-guide-body h3', 'Save up to 4 meal photos.'], 386 ['#diet-guide-status', 'Add your first meal photo to start todayâs food log.'], 387 ['#diet-guide-badge', '0/4'], 388 ['#diet-guide-body .record-flow-actions button:nth-child(1)', 'Take photo'], 389 ['#diet-guide-body .record-flow-actions button:nth-child(2)', 'Choose photo'], 390 ['#diet-guide-body .record-flow-actions button:nth-child(3)', 'Enter fasting metrics'], 391 ['#diet .card h3', 'ð½ï¸ Meal photos'], 392 ['#diet-box-breakfast .upload-area > span:first-child', 'Meal 1'], 393 ['#diet-box-lunch .upload-area > span:first-child', 'Meal 2'], 394 ['#diet-box-dinner .upload-area > span:first-child', 'Meal 3'], 395 ['#diet-box-snack .upload-area > span:first-child', 'Meal 4'], 396 ['#txt-breakfast', 'Upload photo'], 397 ['#txt-lunch', 'Upload photo'], 398 ['#txt-dinner', 'Upload photo'], 399 ['#txt-snack', 'Upload photo'], 400 ['#ai-btn-breakfast', '⨠AI analysis'], 401 ['#ai-btn-lunch', '⨠AI analysis'], 402 ['#ai-btn-dinner', '⨠AI analysis'], 403 ['#ai-btn-snack', '⨠AI analysis'], 404 ['.card-fasting h3', 'ð Morning fasting metrics'], 405 ['#fasting-graph-card h3', 'ð Fasting trends (last 30 days)'], 406 ['#exercise .record-flow-kicker', 'Guide'], 407 ['#exercise .simple-mode-record-title', 'Log your exercise today'], 408 ['#exercise-guide-body h3', 'Steps, workout photos, or workout videos all count.'], 409 ['#exercise-guide-status', 'Add steps, a workout photo, or a workout video to save todayâs exercise log.'], 410 ['#exercise-guide-badge', '0 ready'], 411 ['#exercise-guide-body .record-flow-actions button:nth-child(1)', 'Enter steps'], 412 ['#exercise-guide-body .record-flow-actions button:nth-child(2)', 'Workout photo'], 413 ['#exercise-guide-body .record-flow-actions button:nth-child(3)', 'Workout video'], 414 ['#exercise .quest-item span:first-child', 'ð Cardio/steps 10+5P, strength 10+5P'], 415 ['#quest-exercise', 'Not yet'], 416 ['#step-card h3', 'ð£ Todayâs steps'], 417 ['#exercise-cardio-title', 'ð¸ Workout photo'], 418 ['#exercise-cardio-date-note', 'ð¸ Photos can only be uploaded on the day they were taken.'], 419 ['#exercise-strength-title', 'ð¹ Workout video'], 420 ['#exercise-strength-date-note', 'ð¹ Videos can only be uploaded on the day they were recorded.'], 421 ['#exercise-strength-date-note + .guide-btn', 'ð¸ Easy 10-second hy
421perlapse guide'], 422 ['#sleep .record-flow-kicker', 'Guide'], 423 ['#sleep .simple-mode-record-title', 'Log your mind today'], 424 ['#sleep-guide-body h3', 'Reset with sleep, meditation, and gratitude.'], 425 ['#mind-guide-status', 'Try a sleep screenshot, meditation, or gratitude journal.'], 426 ['#mind-guide-badge', '0 ready'], 427 ['#sleep-guide-body .record-flow-actions button:nth-child(1)', 'Sleep screenshot'], 428 ['#sleep-guide-body .record-flow-actions button:nth-child(2)', 'Start meditation'], 429 ['#sleep-guide-body .record-flow-actions button:nth-child(3)', 'Gratitude journal'], 430 ['#sleep .quest-item span:first-child', 'ð§ Mind/sleep log (10P each, up to 20P)'], 431 ['#quest-mind', 'Not yet'], 432 ['#sleep .upload-cta-text strong', 'Upload a sleep screenshot'], 433 ['#sleep .upload-cta-text span', 'AI will analyze your sleep pattern and suggest improvements.'], 434 ['#sleep .card h3', 'ð Sleep analysis'], 435 ['#sleep-date-note', 'ð¸ Photos can only be uploaded on the day they were taken.'], 436 ['#txt-sleep', 'Upload screenshot'], 437 ['#ai-btn-sleep', '⨠AI analysis'], 438 ['#meditation-card-title', 'ð§ Todayâs meditation'], 439 ['#meditation-practice-card .meditation-practice-kicker', 'Breathing guide'], 440 ['.meditation-common-note', 'Sit comfortably. Do not force it. Stop if you feel dizzy.'], 441 ['.meditation-journal-label', 'Three-line gratitude journal after meditation'], 442 ['#gratitude-voice-btn', 'Voice input'] 443]; 444 445// íë©´ì ê°ì ììê° ì¬ë¬ ë² ìë ê³³(ë²ì ì¤ìì²ë ë¡ê·¸ì¸ íë©´ê³¼ íë¡í íì íëì©). 446// setText ë 첫 ë²ì§¸ë§ ë°ê¾¸ë¯ë¡, ì ë¶ ë°ê¿ì¼ íë ì íìë ì¬ê¸° ëë¤. 447// (기본 목ë¡ì íµì§¸ë¡ querySelectorAll ë¡ ë°ê¾¸ë©´ '#diet .card h3' ê°ì ì íìê° 448// ìëì¹ ìì ììê¹ì§ ë®ì´ì´ë¤.) 449const SELECTOR_TEXTS_ALL = [ 450 ['.version-switcher [data-version="ko"]', 'Full'], 451 ['.version-switcher [data-version="simple"]', 'Simple'], 452 ['.version-switcher [data-version="en"]', 'English'], 453 ['.version-switcher [data-version="app"]', 'Lite'], 454 // íë¡í í ì½ê´Â·ë°©ì¹¨ ë§í¬ í ì¤í¸ (ë¡ê·¸ì¸ í면·íë¡íì ê°ê° ìì ì ìì) 455 ['a[href="/terms.html"]', 'Terms'], 456 ['a[href="/privacy.html"]', 'Privacy'] 457]; 458 459const SELECTOR_ATTRS = [ 460 // íë©´ì ë³´ì´ë ê¸ìë ë¤ ìì´ì¸ë° ì¤í¬ë¦°ë¦¬ëê° ì½ë ê°ë§ íêµì´ë¡ ë¨ì ììë¤. 461 ['#login-modal > img', 'alt', 'Habit School logo'], 462 ['#invite-landing-card', 'alt', 'Record card shared by the friend who invited you'], 463 ['#loginBtn', 'aria-label', 'Sign in with Google'], 464 ['.version-switcher', 'aria-label', 'App version'], 465 ['#guest-demo-entry-btn', 'aria-label', 'Try Habit School without signing in'], 466 ['#selected-date', 'aria-label', 'Date'], 467 ['#weight', 'placeholder', 'Weight (kg)'], 468 ['#glucose', 'placeholder', 'Fasting glucose (mg/dL)'], 469 ['#bp-systolic', 'placeholder', 'Systolic BP'], 470 ['#bp-diastolic', 'placeholder', 'Diastolic BP'], 471 ['#step-manual-input', 'placeholder', 'Enter manually'], 472 ['#sleep-img', 'aria-label', 'Upload sleep screenshot'], 473 ['#preview-breakfast', 'alt', 'Meal 1 preview'], 474 ['#preview-lunch', 'alt', 'Meal 2 preview'], 475 ['#preview-dinner', 'alt', 'Meal 3 preview'], 476 ['#preview-snack', 'alt', 'Meal 4 preview'], 477 ['#preview-sleep', 'alt', 'Sleep screenshot preview'], 478 ['#rm-breakfast', 'aria-label', 'Remove meal 1 photo'], 479 ['#rm-lunch', 'aria-label', 'Remove meal 2 photo'], 480 ['#rm-dinner', 'aria-label', 'Remove meal 3 photo'], 481 ['#rm-snack', 'aria-label', 'Remove meal 4 photo'], 482 ['#rm-sleep', 'aria-label', 'Remove sleep screenshot'], 483 ['#user-greeting', 'aria-label', 'Open profile'], 484 // í ë²í¼ aria-label (íêµì´ê° ê·¸ëë¡ ë¨ì ììì) 485 ['button[onclick="openTab(\'diet\')"]', 'aria-label', 'Diet'], 486 ['button[onclick="openTab(\'exercise\')"]', 'aria-label', 'Exercise'], 487 ['button[onclick="openTab(\'sleep\')"]', 'aria-label', 'Mind'], 488 ['button[onclick="openTab(\'dashboard\')"]', 'aria-label', 'My records'], 489 ['button[onclick="openTab(\'assets\')"]', 'aria-label', 'Assets'], 490 ['#btn-tab-gallery', 'aria-label', 'Gallery'], 491 // ê¸°ë¡ ê°ì´ë ì¹´ë aria-label 492 ['[data-record-guide="diet"]', 'aria-label', 'Diet logging guide'], 493 ['[data-record-guide="exercise"]', 'aria-label', 'Exercise logging guide'], 494 ['[data-record-guide="sleep"]', 'aria-label', 'Mind & sleep logging guide'], 495 // ì¤ë¥ ì ë³´ ë²í¼ 496 ['#bug-report-fab', 'aria-label', 'Report a bug'], 497 // ìë©´ ìê° ì ë ¥ 498 ['#sleep-hours', 'placeholder', 'e.g. 7.5'], 499 ['#shared-import-preview', 'alt', 'Shared photo preview'], 500 ['#share-preview-thumb', 'alt', 'Share preview'], 501 ['#bug-report-modal .close-btn', 'aria-label', 'Close'], 502 ['#share-platform-modal .share-modal-close-x', 'aria-label', 'Close'], 503 // íë¡í í ì½ê´Â·ë°©ì¹¨ ë§í¬: íêµì´ URL â ì문 URL 504 ['a[href="/terms.html"]', 'href', '/en/terms'], 505 ['a[href="/privacy.html"]', 'href', '/en/privacy'] 506]; 507 508function interpolate(template = '', params = {}) { 509 return String(template).replace(/\{(\w+)\}/g, (_, key) => ( 510 params[key] == null ? '' : String(params[key]) 511 )); 512} 513 514export function getLocale() { 515 return getRouteLocale(); 516} 517 518export function isEnglishLocale() { 519 return getLocale() === ENGLISH_LOCALE; 520} 521 522export function t(key, params = {}) { 523 const locale = getLocale(); 524 const table = MESSAGES[locale] || MESSAGES[DEFAULT_LOCALE]; 525 const fallback = MESSAGES[DEFAULT_LOCALE] || {}; 526 return interpolate(table[key] || fallback[key] || key, params); 527} 528 529export function formatDate(value = new Date(), options = {}) { 530 const date = value instanceof Date ? value : new Date(value); 531 const locale = isEnglishLocale() ? 'en-US' : 'ko-KR'; 532 return new Intl.DateTimeFormat(locale, { 533 timeZone: options.timeZone || 'Asia/Seoul', 534 ...options 535 }).format(date); 536} 537 538export { buildLocalizedUrl }; 539 540export function translateText(value = '') { 541 if (!isEnglishLocale()) return value; 542 const raw = String(value || ''); 543 const trimmed = raw.trim(); 544 if (!trimmed) return value; 545 return ENGLISH_TEXT_REPLACEMENTS.get(trimmed) || value; 546} 547 548function setText(doc, selector, value) { 549 const el = doc.querySelector(selector); 550 if (!el || el.dataset.i18nLocked === 'true') return; 551 if (String(el.textContent || '').trim() === value) return; 552 el.textContent = value; 553} 554 555function setTextAll(doc, selector, value) { 556 doc.querySelectorAll(selector).forEach((el) => { 557 if (el.dataset.i18nLocked === 'true') return; 558 if (String(el.textContent || '').trim() === value) return; 559 el.textContent = value; 560 }); 561} 562 563function setAttr(doc, selector, attr, value) { 564 const el = doc.querySelector(selector); 565 if (!el) return; 566 if (el.getAttribute(attr) === value) return; 567 el.setAttribute(attr, value); 568} 569 570function replaceExactTextNodes(root) { 571 if (!root || !isEnglishLocale() || typeof document === 'undefined' || typeof document.createTreeWalker !== 'function') return; 572 const filter = window.NodeFilter || NodeFilter; 573 const walker = document.createTreeWalker(root, filter.SHOW_TEXT, { 574 acceptNode(node) { 575 const text = String(node.nodeValue || '').trim(); 576 if (!text || !ENGLISH_TEXT_REPLACEMENTS.has(text)) return filter.FILTER_REJECT; 577 const parent = node.parentElement; 578 if (!parent || parent.closest('script,style,noscript,template')) return filter.FILTER_REJECT; 579 return filter.FILTER_ACCEPT; 580 } 581 }); 582 const nodes = []; 583 while (walker.nextNode()) nodes.push(walker.currentNode);
584 nodes.forEach((node) => { 585 const original = String(node.nodeValue || ''); 586 const trimmed = original.trim(); 587 node.nodeValue = original.replace(trimmed, ENGLISH_TEXT_REPLACEMENTS.get(trimmed)); 588 }); 589} 590 591export function applyDomTranslations(doc = document) { 592 if (!isEnglishLocale()) return; 593 594 SELECTOR_TEXTS.forEach(([selector, value]) => setText(doc, selector, value)); 595 SELECTOR_TEXTS_ALL.forEach(([selector, value]) => setTextAll(doc, selector, value)); 596 SELECTOR_ATTRS.forEach(([selector, attr, value]) => setAttr(doc, selector, attr, value)); 597 598 doc.querySelectorAll('[data-i18n]').forEach((el) => { 599 const key = el.getAttribute('data-i18n'); 600 if (key) el.textContent = t(key); 601 }); 602 // ëì 문구ì²ë¼ ìì ë§í¬ê° ìë ê³³ì textContentë¡ ê°ìë¼ì°ë©´ ë§í¬ê° ì¬ë¼ì§ë¤. 603 // ê°ì ì´ íì¼ ìì ììë¿ì´ë¼(ì¬ì©ì ì ë ¥ì´ ìëë¤) innerHTMLë¡ ë£ì´ë ìì íë¤. 604 doc.querySelectorAll('[data-i18n-html]').forEach((el) => { 605 const key = el.getAttribute('data-i18n-html'); 606 if (!key) return; 607 const html = t(key); 608 // ê°ì ê°ì ë¤ì ë£ì¼ë©´ ì쪽 ë§í¬ê° íµì§¸ë¡ ìë¡ ë§ë¤ì´ì§ë¤. ë르ë ëì¤ì 609 // ê·¸ë¬ë©´ í´ë¦ì´ ì¬ë¼ì§ ììì ë¨ëë¤. 610 if (el.innerHTML === html) return; 611 el.innerHTML = html; 612 }); 613 doc.querySelectorAll('[data-i18n-placeholder]').forEach((el) => { 614 const key = el.getAttribute('data-i18n-placeholder'); 615 if (key) el.setAttribute('placeholder', t(key)); 616 }); 617 doc.querySelectorAll('[data-i18n-aria-label]').forEach((el) => { 618 const key = el.getAttribute('data-i18n-aria-label'); 619 if (key) el.setAttribute('aria-label', t(key)); 620 }); 621 622 ['diet', 'exercise', 'sleep', 'profile', 'submit-bar'].forEach((id) => { 623 replaceExactTextNodes(doc.getElementById(id)); 624 }); 625} 626 627let localeObserver = null; 628let localeTranslateQueued = false; 629 630export function markLocaleReady(doc = document) { 631 doc.documentElement?.classList.remove('locale-loading'); 632} 633 634export function installLocaleDomObserver(doc = document) { 635 if (!isEnglishLocale() || localeObserver || typeof MutationObserver !== 'function') { 636 markLocaleReady(doc); 637 return; 638 } 639 const run = () => { 640 localeTranslateQueued = false; 641 applyDomTranslations(doc); 642 markLocaleReady(doc); 643 }; 644 localeObserver = new MutationObserver(() => { 645 if (localeTranslateQueued) return; 646 localeTranslateQueued = true; 647 requestAnimationFrame(run); 648 }); 649 localeObserver.observe(doc.body || doc.documentElement, { 650 childList: true, 651 subtree: true, 652 characterData: true 653 }); 654 run(); 655} 656 657if (typeof window !== 'undefined') { 658 window.HabitSchoolI18n = { 659 getLocale, 660 t, 661 formatDate, 662 buildLocalizedUrl, 663 applyDomTranslations, 664 translateText, 665 isEnglishLocale 666 }; 667}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.