// UI language: English by default, Indonesian on request. A module-level store so API helpers // and the instruction parser can translate too, not just components. import { useSyncExternalStore } from 'react' export type Lang = 'en' | 'id' export const LANGS: { code: Lang; label: string }[] = [{ code: 'en', label: 'EN' }, { code: 'id', label: 'ID' }] const en = { // Layout 'nav.brain': 'Brain', 'nav.instruction': 'Instruction', 'nav.schedule': 'Schedule', 'nav.results': 'Results', 'nav.dashboard': 'Dashboard', 'nav.settings': 'Settings', 'nav.production': 'Production', 'layout.learningSpace': 'Learning space', 'layout.switchOrg': 'Switch organization', 'layout.breadcrumb': 'Workspace', 'layout.signOut': 'Sign out', 'layout.engine': 'learning-lab engine', 'layout.creditsTitle': 'Workspace credit balance', 'layout.toStudio': 'Switch to Learning Lab', 'layout.devStrip': 'Development phase: connected to the learning-lab engine. Review AI output before using it.', 'layout.footer': 'A workspace for knowledge that keeps growing.', 'layout.mainNav': 'Main navigation', 'layout.language': 'Language', // Auth 'auth.loading': 'Loading…', 'auth.eyebrow': 'NEW LEARNING, WITHOUT STARTING FROM SCRATCH', 'auth.headline1': 'Team knowledge.', 'auth.headline2': 'New ideas.', 'auth.headline3': 'Learning that never stops.', 'auth.copy': 'Upload material to the Brain, give one instruction, and AI keeps creating new learning content on schedule.', 'auth.benefitUpload': 'Upload material to the Brain', 'auth.benefitSchedule': 'Schedule research and content', 'auth.benefitDownload': 'Open and download the results', 'auth.footer': 'Your knowledge. New possibilities.', 'auth.pickOrg': 'Choose an organization', 'auth.pickOrgCopy': 'Express Mode runs inside an organization so the Brain, schedules, and credits are shared with your team.', // Shared 'common.loading': 'Loading…', 'common.save': 'Save', 'common.cancel': 'Cancel', 'common.retry': 'Try again', 'common.credits': '{n} credits', // Production 'prod.title': 'Production', 'prod.description': 'Everything Express is producing from the Brain: material, content made, what is live, and what is being made right now.', 'prod.metricWorking': 'Content in progress', 'prod.metricWorkingSub': 'being researched or generated now', 'prod.metricMaterial': 'Material', 'prod.metricMaterialSub': '{n} in the Brain', 'prod.metricMade': 'Content made', 'prod.metricMadeSub': 'results the AI has produced', 'prod.metricLive': 'In production', 'prod.metricLiveSub': 'approved for use', 'prod.metricLms': 'On the LMS', 'prod.metricLmsSub': 'approved SCORM modules', 'prod.graphTitle': 'Knowledge graph', 'prod.clickNode': 'Click a node', 'prod.loadingGraph': 'Drawing the knowledge graph…', 'prod.graphLabel': 'Knowledge graph of {nodes} nodes', 'prod.hub.sources': 'Sources', 'prod.hub.made': 'Made', 'prod.hub.working': 'In progress', 'prod.hub.research': 'Research', 'prod.hub.schedules': 'Repeats', 'prod.nodes': 'Nodes', 'prod.nodesSub': 'in this Brain', 'prod.links': 'Links', 'prod.linksSub': 'connections between nodes', 'prod.scheduleTitle': 'Schedule', 'prod.noSchedules': 'No schedules yet.', 'prod.giveInstruction': 'Give an instruction', 'prod.nowTitle': 'Happening now', 'prod.nothingNow': 'Nothing is being made right now.', 'prod.more': '+{n} more in Results', // Brain 'brain.title': 'Brain', 'brain.description': 'All team material is processed into one Brain that keeps growing. New content the AI creates goes in here too.', 'brain.metricMaterial': 'Material', 'brain.metricMaterialSub': 'documents in the workspace', 'brain.metricReady': 'In the Brain', 'brain.metricReadySub': 'knowledge base ready', 'brain.metricAction': 'Needs action', 'brain.metricActionSub': 'waiting for your approval', 'brain.listTitle': 'Material', 'brain.loading': 'Loading material…', 'brain.engineDown': 'Cannot reach the engine', 'brain.empty': 'No material yet', 'brain.emptyCopy': 'Upload documents to start filling the Brain.', 'brain.uploadTitle': 'Upload material to the Brain', 'brain.uploadCopy': 'Drag files anywhere on this page, or choose them from your computer. The engine reads them and adds them to the Brain.', 'brain.formats': 'Formats: {list}', 'brain.formatsLoading': 'Loading supported formats…', 'brain.chooseFiles': 'Choose files', 'brain.uploading': 'Uploading…', 'brain.uploaded': 'Uploaded', 'brain.unsupported': 'Unsupported format', 'brain.dropHere': 'Drop files to upload them to the Brain', 'brain.statusReady': 'In the Brain', 'brain.statusBuilding': 'Building Brain', 'brain.statusProcessing': 'Processing', 'brain.statusAnswer': 'Needs action', 'brain.statusFailed': 'Failed', 'brain.queued': 'Waiting in queue…', 'brain.versionWithCandidate': 'Looks like “{name}”. Is this a new version of it?', 'brain.versionGeneric': 'Is this a new version of an existing document?', 'brain.versionYes': 'Yes, new version', 'brain.versionNo': 'No, keep separate', 'brain.needsCredits': 'Not enough credits to add this to the Brain{need}. Top up, then try again.', 'brain.needsCreditsNeed': ' (needs {credits})', 'brain.failed': 'Could not add this to the Brain.', 'brain.retryWith': 'Try again ({credits})', 'brain.failedAfterBuild': 'Processing this file failed after its knowledge was built. Schedules that use the whole Brain stop until it is removed: delete it and upload it again.', 'brain.confirmCopy': 'Adding this document to the Brain uses {credits} from the workspace.', 'brain.confirmCreditsFallback': 'credits', 'brain.addToBrain': 'Add to Brain', 'brain.delete': 'Delete {name}', 'brain.open': 'Open {name}', 'brain.deleteConfirm': 'Delete “{name}” from the Brain?', // Instruction 'ins.title': 'Instruction', 'ins.description': 'Write a plain instruction. The AI turns it into research and content schedules that keep running until you stop them.', 'ins.label': 'Instruction', 'ins.ex1': 'Make 2 summaries per day and 1 quiz per week about leadership', 'ins.ex2': 'Research the internet every day about leadership', 'ins.ex3': '3 roleplays per week about negotiating with customers', 'ins.ex4': 'Research every Monday about sales trends, then make 5 flashcards per week', 'ins.ex5': '1 video per week, 2 podcasts per week and 1 SCORM module per week about onboarding', 'ins.previewTitle': 'Schedules to create', 'ins.readyCount': '{n} ready', 'ins.approveNote': 'Once approved, schedules start right away. Content is created from the Brain without repeating existing content, using workspace credits within the limits above.', 'ins.approve': 'Approve {n} schedules', 'ins.approving': 'Creating schedules…', 'ins.active': 'Active', 'ins.failed': 'Failed', 'ins.viewSchedule': 'View in Schedule', 'ins.kind': 'Content type', 'ins.count': 'Count', 'ins.frequency': 'Frequency', 'ins.perDay': 'per day', 'ins.perWeek': 'per week', 'ins.researchFrequency': 'Research frequency', 'ins.everyDay': 'Every day', 'ins.everyWeek': 'Every week', 'ins.weekday': 'Day', 'ins.topic': 'Topic', 'ins.topicPlaceholder': 'Topic (empty = whole Brain)', 'ins.autoApprove': 'New sources go straight into the Brain (no review first)', 'ins.notCreated': 'This schedule will not be created yet.', 'ins.tooMany': 'At most 50 items per week for one schedule.', 'ins.budget': 'Credit limit: {first}, {monthly} per month.', 'ins.budgetWeekly': '{n} per week', 'ins.budgetRun': '{n} per research run', 'ins.removeRule': 'Remove from list', // Rules (parser output) 'rule.everyDay': 'Every day', 'rule.everyWeekday': 'Every {day}', 'rule.research': '{when}: internet research{about}', 'rule.about': ' about {topic}', 'rule.wholeBrain': ' from the whole Brain', 'rule.content': '{kind}: {count} {per}{about}', 'rule.monthNote': '{count} per month is scheduled as {weekly} per week.', 'kind.summary': 'Summary', 'kind.quiz': 'Quiz', 'kind.flashcards': 'Flashcards', 'kind.roleplay': 'Roleplay', 'kind.simulator': 'Simulation', 'kind.slide_deck': 'Slides', 'kind.video': 'Video', 'kind.audio': 'Audio', 'kind.scorm': 'SCORM', 'day.0': 'Monday', 'day.1': 'Tuesday', 'day.2': 'Wednesday', 'day.3': 'Thursday', 'day.4': 'Friday', 'day.5': 'Saturday', 'day.6': 'Sunday', // Schedule 'sch.title': 'Schedule', 'sch.description': 'Research and content schedules that are running. Change the topic any time; the next content follows the new topic.', 'sch.newInstruction': 'New instruction', 'sch.cardTitle': 'Schedules', 'sch.loading': 'Loading schedules…', 'sch.loadFailed': 'Could not load schedules', 'sch.empty': 'No schedules yet', 'sch.emptyCopy': 'Give an instruction like “2 summaries per day about leadership” to start.', 'sch.nextResearch': 'Next research: {when}', 'sch.paused': 'Paused', 'sch.active': 'Active', 'sch.newTopic': 'New topic', 'sch.changeTopic': 'Change topic', 'sch.pause': 'Pause', 'sch.resume': 'Resume', 'sch.runNow': 'Run now', 'sch.history': 'History', 'sch.hideHistory': 'Hide history', 'sch.delete': 'Delete', 'sch.deleteConfirm': 'Delete the schedule “{name}”? Content already created stays.', 'sch.loadingProgress': 'Loading progress…', 'sch.planningOff': 'Automatic production is off in the engine: an admin needs to set a price for “agent.content.plan”.', 'sch.produceNow': 'Produce now', 'prod.runningTitle': 'Producing {n} new pieces', 'prod.lastTitle': 'Last production', 'prod.count': '{ready} of {total} ready', 'prod.stepTopics': 'AI chooses new topics', 'prod.stepStart': 'Starting', 'prod.stepCreate': 'Creating content ({ready}/{total})', 'prod.stepDone': 'Done', 'prod.failed': 'Production failed ({code}).', 'prod.someFailed': '{n} failed.', 'prod.shortfall': 'The Brain had new material for fewer pieces than asked ({n} short); add documents for more topics.', 'prod.viewResults': 'View in Results', 'sch.produceHowMany': 'How many pieces now? (up to {max} left this week)', 'sch.checkPrice': 'Check price', 'sch.checkingPrice': 'Checking…', 'sch.produceQuote': 'Planning {n} pieces costs {credits} credits. Each piece is then charged as it is created, within this schedule’s weekly limit of {budget} credits. Production starts within about a minute.', 'sch.startProduction': 'Start production', 'sch.starting': 'Starting…', 'sch.alreadyProducing': 'This schedule is already producing. Wait for it to finish first.', 'sch.weekFull': 'This week’s target is already used up. The next pieces come with next week’s schedule.', 'sch.thisWeek': 'This week: {ready} of {target} ready{failed}', 'sch.thisWeekFailed': ', {n} failed', 'sch.waitingFirst': 'Waiting for the first production slot', 'sch.loadingHistory': 'Loading history…', 'sch.neverRan': 'Has not run yet.', 'sch.scheduled': 'Scheduled', 'sch.manual': 'Manual', 'run.queued': 'Queued', 'run.planning': 'Planning', 'run.running': 'Running', 'run.completed': 'Completed', 'run.failed': 'Failed', 'run.cancelled': 'Cancelled', // Results 'res.title': 'Results', 'res.description': 'Everything Express creates: text, quizzes, roleplays, video, audio, and SCORM.', 'res.empty': 'No results yet', 'res.emptyCopy': 'Content and sources from your schedules appear here. Give an instruction to start a schedule.', 'res.tabContent': 'Content', 'res.tabResearch': 'Research', 'res.pendingCount': '{n} to review', 'res.allContent': 'All content', 'res.foundSources': 'Sources found by research', 'res.search': 'Search titles…', 'res.filterKind': 'Filter by type', 'res.filterStatus': 'Filter by status', 'res.allKinds': 'All types', 'res.status.all': 'All statuses', 'res.status.review': 'Needs review', 'res.status.approved': 'Approved', 'res.status.working': 'In progress', 'res.status.failed': 'Failed or rejected', 'res.state.preparing': 'Preparing', 'res.state.generating': 'Generating', 'res.state.ready': 'Needs review', 'res.state.accepted': 'Approved', 'res.state.failed': 'Failed', 'res.state.rejected': 'Rejected', 'res.source.pending': 'Needs review', 'res.source.accepted': 'Added to Library', 'res.source.rejected': 'Rejected', 'res.loadFailed': 'Results could not be loaded', 'res.noMatch': 'Nothing matches', 'res.noMatchCopy': 'Try another keyword, type, or status.', 'res.noteContent': 'Download as PDF, Markdown or SCORM; slide decks also as PowerPoint, videos as MP4, audio scripts as text. Roleplay and simulation run interactively in Learning Studio.', 'res.noteResearch': 'Research finds web sources. Add the useful ones to the Library, then build their knowledge on the Brain page.', 'res.sourceCount': '{n} sources', 'res.sourceOne': '1 source', 'res.working': 'Being created…', 'res.close': 'Close', 'res.reviewNotice': 'AI-generated draft. Check it, then approve it or reject it.', 'res.noContent': 'No content to show', 'res.noContentCopy': 'This item has not finished or its output is no longer available.', 'res.info': 'Content details', 'res.statusLabel': 'Status', 'res.schedule': 'Schedule', 'res.created': 'Created', 'res.sources': 'Sources', 'res.footNote': 'Created by Express from your Brain.', 'res.openStudio': 'Open in Learning Studio', 'res.download': 'Download', 'res.preparingFile': 'Preparing file…', 'res.pdfHint': 'Opens the print dialog: choose “Save as PDF”', 'res.pptxHint': 'Slides rendered by Learning Studio', 'res.mdHint': 'Plain text with headings, for editing', 'res.accept': 'Approve', 'res.reject': 'Reject', 'res.addToBrain': 'Add to Library', 'res.inBrain': 'see Brain', 'res.answer': 'Answer', 'res.slide': 'Slide {n}', 'res.field.description': 'Description', 'res.field.topic': 'Topic', 'res.field.objective': 'Objective', 'res.field.learning_objectives': 'Learning objectives', 'res.field.user_role': 'Learner role', 'res.field.ai_character': 'AI character', 'res.field.difficulty': 'Difficulty', 'res.field.context': 'Context', // Media and SCORM (Phases 4, 5, 7) 'ins.noteVideo': 'One video per run, about {seconds} seconds in {scenes} scenes, made by Video Studio. Each video waits for your review in Results.', 'ins.noteAudio': 'Written as a narration script. Results plays it with the device voice and downloads it as text (no MP3 yet: the engine has no audio service for agents).', 'ins.noteScorm': 'Each module is a lesson with a 10-question quiz, downloaded from Results as a SCORM 1.2 package for your LMS.', 'ins.videoCadence': 'A video schedule makes one video per run: choose 1 per day or 1 per week.', 'ins.videoQuoteFailed': 'The video price could not be checked: {message}', 'ins.budgetVideo': '{n} per video', 'agent.name': 'Express Agent', 'agent.status': 'Asks when something is unclear', 'agent.statusThinking': 'Thinking…', 'agent.think1': 'Reading your message', 'agent.think2': 'Checking the plan', 'agent.think3': 'Working out what is still unclear', 'agent.thought': 'Thought for {s}s', 'agent.greeting': 'Hi! Tell me what to create from the Brain and how often, or what to research on the internet. If anything is unclear, I will ask you here.', 'agent.understood': 'Got it. Here is the plan so far:\n{list}', 'agent.added': 'Added to the plan:\n{list}', 'agent.notUnderstood': 'I could not find a content type or research request in that. Try something like “2 summaries per day about leadership”.', 'agent.unclear': 'Sorry, I did not catch that. Pick one of the options below or answer in your own words.', 'agent.askUnknown': 'I did not understand “{part}”. What did you mean? Rephrase it, or skip it.', 'agent.askVideo': '{rule}: a video schedule makes one video per run, so it can be 1 per week or 1 per day. Which one?', 'agent.askTooMany': '{rule} is more than 50 per week, the limit for one schedule. How many per week should it be?', 'agent.askWeekday': 'Weekly internet research{about}: which day should it run? It is set to Monday for now.', 'agent.askTopic': 'What topic should this be about?\n{list}\nType a topic, or use the whole Brain.', 'agent.ready': 'Everything is clear: {n} schedules are ready. You can still change them in the plan. Shall I start them?', 'agent.readyLanguage': 'Everything is clear: {n} schedules are ready, with content written in {language}. You can still change them in the plan. Shall I start them?', 'agent.askLanguage': 'Which language should the content be written in?', 'agent.languageSet': 'Content will be written in {language}.', 'ins.language': 'Content language', 'ins.languagePending': 'Not chosen yet', 'agent.skipped': 'OK, I skipped “{part}”.', 'agent.removed': 'Removed {rule}.', 'agent.updated': 'Updated: {rule}.', 'agent.topicSet': 'Topic set to “{topic}”.', 'agent.wholeBrain': 'OK, the whole Brain it is.', 'agent.reset': 'Starting over. What should I create?', 'agent.approving': 'Creating the schedules…', 'agent.done': 'Done: {n} schedules are active and start right away.', 'agent.partial': '{ok} of {n} schedules were created. The ones that failed are still in the plan: fix them, then approve again.', 'agent.optSkip': 'Skip', 'agent.optRemove': 'Remove it', 'agent.optWholeBrain': 'Whole Brain', 'agent.optApprove': 'Yes, start them', 'agent.optReset': 'Start over', 'agent.optPerWeek': '1 per week', 'agent.optPerDay': '1 per day', 'agent.opt50': '50 per week', 'agent.placeholder': 'Reply to the agent or give a new instruction…', 'agent.send': 'Send', 'agent.hint': 'Enter to send, Shift+Enter for a new line.', 'agent.planEmpty': 'The plan fills in as you chat with the agent.', 'sch.nextVideo': 'Next video: {when}', 'sch.videoNow': 'Make a video now', 'sch.videoStarted': 'Video started. It shows in Results while it is being made.', 'sch.runStarted': 'Research started.', 'sch.deleted': 'Schedule deleted.', 'res.videoProgress': 'Rendering… {n}%', 'res.videoNotReady': 'The video is not ready yet', 'res.videoFailed': 'The video could not be made', 'res.videoFailedCopy': 'Rendering stopped before the video was finished.', 'res.downloadVideo': 'Download video (MP4)', 'res.creditsUsed': 'Credits used', 'res.accepted': 'Approved.', 'res.rejected': 'Rejected.', 'res.retry': 'Retry', 'res.retrying': 'Retrying…', 'res.retried': 'Production started again.', 'res.audioNotice': 'Played with this device’s voice, which can differ between devices. Download the script to record it or use it in a TTS tool.', 'res.fullScript': 'Show the full script', 'res.scriptTxt': 'Script (.txt)', 'res.scriptHint': 'Narration text, for recording or a TTS tool', 'res.quiz': 'Quiz', 'res.quizPending': 'The quiz is not ready yet.', 'res.scormModule': 'SCORM 1.2 module', 'res.scormMeta': 'Lesson + {n} questions · pass mark {mastery}%', 'res.scormHint': 'For an LMS: the lesson, marked complete when finished', 'res.scormQuizHint': 'For an LMS: lesson and quiz, reports score and pass/fail', 'res.scormReady': 'SCORM package downloaded. Upload the .zip to your LMS.', 'scorm.submit': 'Check answers', 'scorm.retry': 'Try again', 'scorm.complete': 'Mark as complete', 'scorm.completed': 'Completed ✓', 'scorm.score': 'Score: {score}%.', 'scorm.passed': 'Passed (pass mark {n}%).', 'scorm.failed': 'Not passed yet (pass mark {n}%).', 'scorm.answerAll': 'Answer every question first.', 'scorm.noLms': 'Opened outside an LMS: progress is not saved.', 'audio.copy': 'The narration script, read aloud by this device.', 'audio.unsupported': 'This browser cannot read text aloud. Download the script instead.', 'audio.play': 'Listen', 'audio.pause': 'Pause', 'audio.resume': 'Resume', 'audio.stop': 'Stop', 'audio.speed': 'Speed', 'audio.ready': '{n} sentences · press Listen to start', 'audio.sentence': 'Sentence {n} of {total}', 'brain.deleted': '“{name}” deleted.', // Guide 'help.boxTitle': 'How Express works', 'help.boxBody': 'Four steps from your material to finished content.', 'help.open': 'View the guide', 'help.title': 'How Learning Lab Express works', 'help.subtitle': 'Brain → Instruction → Schedule → Results', 'help.brainTitle': 'Brain', 'help.brainBody': 'Upload your training material. Once its knowledge is built, the AI creates everything from it.', 'help.instructionTitle': 'Instruction', 'help.instructionBody': 'Say what you need in one sentence, e.g. “3 quizzes per week and 1 video per week about leadership”. Check the schedules, then approve them.', 'help.scheduleTitle': 'Schedule', 'help.scheduleBody': 'Schedules run on their own. Pause them, change the topic, or produce now; each stays within its credit limit.', 'help.resultsTitle': 'Results', 'help.resultsBody': 'Review what was made, approve or reject it, and download it: PDF, Markdown, PowerPoint, MP4, audio script, or SCORM for your LMS.', 'help.note': 'Content is AI-generated: always review it before using it. Credits come from your organization’s workspace.', 'help.done': 'Got it', // Errors from the engine client 'err.unreachable': 'Cannot reach the learning-lab engine.', 'err.generic': 'The request to the engine failed.', 'err.credits': 'Not enough credits: needs {required}, balance {balance}.', 'err.creditsShort': 'Not enough credits.', 'err.docFailed': 'The document could not be processed.', 'err.timeout': 'Document processing took too long.', 'err.researchFailed': 'Research could not run ({code}).', } as const export type Key = keyof typeof en const id: Record = { 'prod.title': 'Produksi', 'prod.description': 'Semua yang diproduksi Express dari Otak: materi, konten yang dibuat, yang sudah tayang, dan yang sedang dibuat saat ini.', 'prod.metricWorking': 'Konten sedang dibuat', 'prod.metricWorkingSub': 'sedang diriset atau dibuat sekarang', 'prod.metricMaterial': 'Materi', 'prod.metricMaterialSub': '{n} sudah di Otak', 'prod.metricMade': 'Konten dibuat', 'prod.metricMadeSub': 'hasil yang sudah dibuat AI', 'prod.metricLive': 'Sudah produksi', 'prod.metricLiveSub': 'disetujui untuk dipakai', 'prod.metricLms': 'Sudah di LMS', 'prod.metricLmsSub': 'modul SCORM yang disetujui', 'prod.graphTitle': 'Knowledge graph', 'prod.clickNode': 'Klik sebuah node', 'prod.loadingGraph': 'Menggambar knowledge graph…', 'prod.graphLabel': 'Knowledge graph berisi {nodes} node', 'prod.hub.sources': 'Sumber', 'prod.hub.made': 'Dibuat', 'prod.hub.working': 'Sedang dibuat', 'prod.hub.research': 'Riset', 'prod.hub.schedules': 'Berulang', 'prod.nodes': 'Node', 'prod.nodesSub': 'di Otak ini', 'prod.links': 'Tautan', 'prod.linksSub': 'koneksi antar node', 'prod.scheduleTitle': 'Jadwal', 'prod.noSchedules': 'Belum ada jadwal.', 'prod.giveInstruction': 'Beri perintah', 'prod.nowTitle': 'Sedang berjalan', 'prod.nothingNow': 'Tidak ada yang sedang dibuat.', 'prod.more': '+{n} lagi di Hasil', 'nav.brain': 'Otak', 'nav.instruction': 'Perintah', 'nav.schedule': 'Jadwal', 'nav.results': 'Hasil', 'nav.dashboard': 'Dasbor', 'nav.settings': 'Pengaturan', 'nav.production': 'Produksi', 'layout.learningSpace': 'Ruang belajar', 'layout.switchOrg': 'Ganti organisasi', 'layout.breadcrumb': 'Ruang kerja', 'layout.signOut': 'Keluar', 'layout.engine': 'Engine learning-lab', 'layout.creditsTitle': 'Saldo kredit workspace', 'layout.toStudio': 'Beralih ke Learning Lab', 'layout.devStrip': 'Fase development: terhubung ke engine learning-lab. Hasil AI perlu ditinjau sebelum dipakai.', 'layout.footer': 'Ruang kerja untuk pengetahuan yang terus berkembang.', 'layout.mainNav': 'Navigasi utama', 'layout.language': 'Bahasa', 'auth.loading': 'Memuat…', 'auth.eyebrow': 'BELAJAR BARU, TANPA MULAI DARI NOL', 'auth.headline1': 'Pengetahuan tim.', 'auth.headline2': 'Ide baru.', 'auth.headline3': 'Pembelajaran tanpa henti.', 'auth.copy': 'Unggah materi ke Otak, beri satu perintah, dan AI terus membuat konten pembelajaran baru sesuai jadwal.', 'auth.benefitUpload': 'Unggah materi ke Otak', 'auth.benefitSchedule': 'Jadwalkan riset dan konten', 'auth.benefitDownload': 'Buka dan unduh hasilnya', 'auth.footer': 'Pengetahuan Anda. Kemungkinan baru.', 'auth.pickOrg': 'Pilih organisasi', 'auth.pickOrgCopy': 'Express Mode berjalan di dalam organisasi agar Otak, jadwal, dan kredit dipakai bersama tim.', 'common.loading': 'Memuat…', 'common.save': 'Simpan', 'common.cancel': 'Batal', 'common.retry': 'Coba lagi', 'common.credits': '{n} kredit', 'brain.title': 'Otak', 'brain.description': 'Semua materi tim diproses menjadi satu Otak yang terus bertambah. Konten baru yang dihasilkan juga masuk ke sini.', 'brain.metricMaterial': 'Materi', 'brain.metricMaterialSub': 'dokumen di ruang kerja', 'brain.metricReady': 'Masuk Otak', 'brain.metricReadySub': 'knowledge base siap', 'brain.metricAction': 'Perlu tindakan', 'brain.metricActionSub': 'menunggu persetujuan Anda', 'brain.listTitle': 'Materi', 'brain.loading': 'Memuat materi…', 'brain.engineDown': 'Engine tidak dapat dihubungi', 'brain.empty': 'Belum ada materi', 'brain.emptyCopy': 'Unggah dokumen untuk mulai mengisi Otak.', 'brain.uploadTitle': 'Unggah materi ke Otak', 'brain.uploadCopy': 'Tarik file ke mana saja di halaman ini, atau pilih dari komputer. Engine membaca isinya lalu menambahkannya ke Otak.', 'brain.formats': 'Format: {list}', 'brain.formatsLoading': 'Memuat format yang didukung…', 'brain.chooseFiles': 'Pilih file', 'brain.uploading': 'Mengunggah…', 'brain.uploaded': 'Terunggah', 'brain.unsupported': 'Format tidak didukung', 'brain.dropHere': 'Lepaskan file untuk diunggah ke Otak', 'brain.statusReady': 'Masuk Otak', 'brain.statusBuilding': 'Membangun Otak', 'brain.statusProcessing': 'Diproses', 'brain.statusAnswer': 'Perlu tindakan', 'brain.statusFailed': 'Gagal', 'brain.queued': 'Menunggu antrean…', 'brain.versionWithCandidate': 'Mirip dengan “{name}”. Apakah ini versi barunya?', 'brain.versionGeneric': 'Apakah ini versi baru dari dokumen yang sudah ada?', 'brain.versionYes': 'Ya, versi baru', 'brain.versionNo': 'Bukan, simpan terpisah', 'brain.needsCredits': 'Kredit tidak cukup untuk memasukkan ke Otak{need}. Isi ulang kredit, lalu coba lagi.', 'brain.needsCreditsNeed': ' (butuh {credits})', 'brain.failed': 'Gagal memasukkan ke Otak.', 'brain.retryWith': 'Coba lagi ({credits})', 'brain.failedAfterBuild': 'Pemrosesan file ini gagal setelah pengetahuannya dibangun. Jadwal yang memakai seluruh Otak berhenti sampai file ini dihapus: hapus lalu unggah ulang.', 'brain.confirmCopy': 'Memasukkan dokumen ini ke Otak memakai {credits} dari ruang kerja.', 'brain.confirmCreditsFallback': 'kredit', 'brain.addToBrain': 'Masukkan ke Otak', 'brain.delete': 'Hapus {name}', 'brain.open': 'Buka {name}', 'brain.deleteConfirm': 'Hapus “{name}” dari Otak?', 'ins.title': 'Perintah', 'ins.description': 'Tulis instruksi biasa. AI menyusunnya menjadi jadwal riset dan pembuatan konten, lalu terus berjalan sampai Anda menghentikannya.', 'ins.label': 'Instruksi', 'ins.ex1': 'Bikin 2 ringkasan per hari dan 1 kuis per minggu soal leadership', 'ins.ex2': 'Riset internet tiap hari soal leadership', 'ins.ex3': '3 roleplay per minggu tentang negosiasi dengan pelanggan', 'ins.ex4': 'Riset tiap Senin soal tren sales, lalu buat 5 flashcard per minggu', 'ins.ex5': '1 video per minggu, 2 podcast per minggu dan 1 modul SCORM per minggu soal onboarding', 'ins.previewTitle': 'Jadwal yang akan dibuat', 'ins.readyCount': '{n} siap', 'ins.approveNote': 'Setelah disetujui, jadwal langsung aktif. Konten dibuat dari materi di Otak tanpa mengulang yang sudah ada, dan memakai kredit ruang kerja sesuai batas di atas.', 'ins.approve': 'Setujui {n} jadwal', 'ins.approving': 'Membuat jadwal…', 'ins.active': 'Aktif', 'ins.failed': 'Gagal', 'ins.viewSchedule': 'Lihat di Jadwal', 'ins.kind': 'Jenis konten', 'ins.count': 'Jumlah', 'ins.frequency': 'Frekuensi', 'ins.perDay': 'per hari', 'ins.perWeek': 'per minggu', 'ins.researchFrequency': 'Frekuensi riset', 'ins.everyDay': 'Setiap hari', 'ins.everyWeek': 'Setiap minggu', 'ins.weekday': 'Hari', 'ins.topic': 'Topik', 'ins.topicPlaceholder': 'Topik (kosong = seluruh Otak)', 'ins.autoApprove': 'Sumber baru langsung masuk ke Otak (tanpa ditinjau dulu)', 'ins.notCreated': 'Jadwal ini belum dibuat.', 'ins.tooMany': 'Maksimal 50 konten per minggu untuk satu jadwal.', 'ins.budget': 'Batas kredit: {first}, {monthly} per bulan.', 'ins.budgetWeekly': '{n} per minggu', 'ins.budgetRun': '{n} per riset', 'ins.removeRule': 'Hapus dari daftar', 'rule.everyDay': 'Setiap hari', 'rule.everyWeekday': 'Setiap {day}', 'rule.research': '{when}: riset internet{about}', 'rule.about': ' tentang {topic}', 'rule.wholeBrain': ' dari seluruh Otak', 'rule.content': '{kind}: {count} {per}{about}', 'rule.monthNote': '{count} per bulan dijadwalkan sebagai {weekly} per minggu.', 'kind.summary': 'Ringkasan', 'kind.quiz': 'Kuis', 'kind.flashcards': 'Flashcard', 'kind.roleplay': 'Roleplay', 'kind.simulator': 'Simulasi', 'kind.slide_deck': 'Slide', 'kind.video': 'Video', 'kind.audio': 'Audio', 'kind.scorm': 'SCORM', 'day.0': 'Senin', 'day.1': 'Selasa', 'day.2': 'Rabu', 'day.3': 'Kamis', 'day.4': 'Jumat', 'day.5': 'Sabtu', 'day.6': 'Minggu', 'sch.title': 'Jadwal', 'sch.description': 'Jadwal riset dan pembuatan konten yang sedang berjalan. Ubah topik kapan saja; konten berikutnya mengikuti topik baru.', 'sch.newInstruction': 'Perintah baru', 'sch.cardTitle': 'Jadwal', 'sch.loading': 'Memuat jadwal…', 'sch.loadFailed': 'Jadwal tidak dapat dimuat', 'sch.empty': 'Belum ada jadwal', 'sch.emptyCopy': 'Beri perintah seperti “2 ringkasan per hari soal leadership” untuk mulai.', 'sch.nextResearch': 'Riset berikutnya: {when}', 'sch.paused': 'Dijeda', 'sch.active': 'Aktif', 'sch.newTopic': 'Topik baru', 'sch.changeTopic': 'Ganti topik', 'sch.pause': 'Jeda', 'sch.resume': 'Lanjutkan', 'sch.runNow': 'Jalankan sekarang', 'sch.history': 'Riwayat', 'sch.hideHistory': 'Sembunyikan riwayat', 'sch.delete': 'Hapus', 'sch.deleteConfirm': 'Hapus jadwal “{name}”? Konten yang sudah dibuat tetap ada.', 'sch.loadingProgress': 'Memuat progres…', 'sch.planningOff': 'Produksi otomatis belum aktif di engine: admin perlu mengatur harga “agent.content.plan”.', 'sch.produceNow': 'Produksi sekarang', 'prod.runningTitle': 'Memproduksi {n} konten baru', 'prod.lastTitle': 'Produksi terakhir', 'prod.count': '{ready} dari {total} siap', 'prod.stepTopics': 'AI memilih topik baru', 'prod.stepStart': 'Memulai', 'prod.stepCreate': 'Membuat konten ({ready}/{total})', 'prod.stepDone': 'Selesai', 'prod.failed': 'Produksi gagal ({code}).', 'prod.someFailed': '{n} gagal.', 'prod.shortfall': 'Brain hanya punya materi baru untuk lebih sedikit konten dari yang diminta (kurang {n}); tambah dokumen untuk topik lain.', 'prod.viewResults': 'Lihat di Results', 'sch.produceHowMany': 'Berapa konten sekarang? (sisa {max} untuk minggu ini)', 'sch.checkPrice': 'Cek harga', 'sch.checkingPrice': 'Mengecek…', 'sch.produceQuote': 'Merencanakan {n} konten memakai {credits} kredit. Setiap konten lalu dipotong kreditnya saat dibuat, dalam batas mingguan jadwal ini sebesar {budget} kredit. Produksi mulai dalam sekitar satu menit.', 'sch.startProduction': 'Mulai produksi', 'sch.starting': 'Memulai…', 'sch.alreadyProducing': 'Jadwal ini sedang berproduksi. Tunggu sampai selesai dulu.', 'sch.weekFull': 'Target minggu ini sudah terpakai. Konten berikutnya ikut jadwal minggu depan.', 'sch.thisWeek': 'Minggu ini: {ready} dari {target} siap{failed}', 'sch.thisWeekFailed': ', {n} gagal', 'sch.waitingFirst': 'Menunggu jadwal produksi pertama', 'sch.loadingHistory': 'Memuat riwayat…', 'sch.neverRan': 'Belum pernah berjalan.', 'sch.scheduled': 'Terjadwal', 'sch.manual': 'Manual', 'run.queued': 'Antre', 'run.planning': 'Merencanakan', 'run.running': 'Berjalan', 'run.completed': 'Selesai', 'run.failed': 'Gagal', 'run.cancelled': 'Dibatalkan', 'res.title': 'Hasil', 'res.description': 'Semua konten yang dihasilkan Express: teks, kuis, roleplay, video, audio, dan SCORM.', 'res.empty': 'Belum ada hasil', 'res.emptyCopy': 'Konten dan sumber dari jadwal Anda tampil di sini. Beri perintah untuk memulai jadwal.', 'res.tabContent': 'Konten', 'res.tabResearch': 'Riset', 'res.pendingCount': '{n} perlu ditinjau', 'res.allContent': 'Semua konten', 'res.foundSources': 'Sumber hasil riset', 'res.search': 'Cari judul…', 'res.filterKind': 'Filter jenis', 'res.filterStatus': 'Filter status', 'res.allKinds': 'Semua jenis', 'res.status.all': 'Semua status', 'res.status.review': 'Perlu ditinjau', 'res.status.approved': 'Disetujui', 'res.status.working': 'Sedang dibuat', 'res.status.failed': 'Gagal atau ditolak', 'res.state.preparing': 'Menyiapkan', 'res.state.generating': 'Membuat', 'res.state.ready': 'Perlu ditinjau', 'res.state.accepted': 'Disetujui', 'res.state.failed': 'Gagal', 'res.state.rejected': 'Ditolak', 'res.source.pending': 'Perlu ditinjau', 'res.source.accepted': 'Masuk Library', 'res.source.rejected': 'Ditolak', 'res.loadFailed': 'Hasil tidak dapat dimuat', 'res.noMatch': 'Tidak ada yang cocok', 'res.noMatchCopy': 'Coba kata kunci, jenis, atau status lain.', 'res.noteContent': 'Unduh sebagai PDF, Markdown, atau SCORM; slide deck juga sebagai PowerPoint, video sebagai MP4, naskah audio sebagai teks. Roleplay dan simulasi dijalankan secara interaktif di Learning Studio.', 'res.noteResearch': 'Riset menemukan sumber dari web. Masukkan yang berguna ke Library, lalu bangun pengetahuannya di halaman Brain.', 'res.sourceCount': '{n} sumber', 'res.sourceOne': '1 sumber', 'res.working': 'Sedang dibuat…', 'res.close': 'Tutup', 'res.reviewNotice': 'Draft buatan AI. Periksa dulu, lalu setujui atau tolak.', 'res.noContent': 'Tidak ada konten untuk ditampilkan', 'res.noContentCopy': 'Item ini belum selesai atau hasilnya sudah tidak tersedia.', 'res.info': 'Informasi konten', 'res.statusLabel': 'Status', 'res.schedule': 'Jadwal', 'res.created': 'Dibuat', 'res.sources': 'Sumber', 'res.footNote': 'Dibuat oleh Express dari Brain Anda.', 'res.openStudio': 'Buka di Learning Studio', 'res.download': 'Unduh', 'res.preparingFile': 'Menyiapkan file…', 'res.pdfHint': 'Membuka dialog cetak: pilih “Save as PDF” / “Simpan sebagai PDF”', 'res.pptxHint': 'Slide dari Learning Studio', 'res.mdHint': 'Teks dengan judul, untuk disunting', 'res.accept': 'Setujui', 'res.reject': 'Tolak', 'res.addToBrain': 'Masukkan ke Library', 'res.inBrain': 'lihat Brain', 'res.answer': 'Jawaban', 'res.slide': 'Slide {n}', 'res.field.description': 'Deskripsi', 'res.field.topic': 'Topik', 'res.field.objective': 'Tujuan', 'res.field.learning_objectives': 'Tujuan pembelajaran', 'res.field.user_role': 'Peran peserta', 'res.field.ai_character': 'Karakter AI', 'res.field.difficulty': 'Tingkat kesulitan', 'res.field.context': 'Konteks', // Media dan SCORM (Fase 4, 5, 7) 'ins.noteVideo': 'Satu video per proses, sekitar {seconds} detik dalam {scenes} adegan, dibuat oleh Video Studio. Setiap video menunggu tinjauan Anda di Hasil.', 'ins.noteAudio': 'Dibuat sebagai naskah narasi. Hasil memutarnya dengan suara perangkat dan mengunduhnya sebagai teks (belum MP3: engine belum punya layanan audio untuk agen).', 'ins.noteScorm': 'Setiap modul berisi materi dan kuis 10 soal, diunduh dari Hasil sebagai paket SCORM 1.2 untuk LMS Anda.', 'ins.videoCadence': 'Jadwal video membuat satu video per proses: pilih 1 per hari atau 1 per minggu.', 'ins.videoQuoteFailed': 'Harga video tidak bisa dicek: {message}', 'ins.budgetVideo': '{n} per video', 'agent.name': 'Agen Express', 'agent.status': 'Bertanya bila ada yang belum jelas', 'agent.statusThinking': 'Sedang berpikir…', 'agent.think1': 'Membaca pesan Anda', 'agent.think2': 'Memeriksa rencana', 'agent.think3': 'Mencari yang masih belum jelas', 'agent.thought': 'Berpikir {s} detik', 'agent.greeting': 'Halo! Ceritakan konten apa yang perlu dibuat dari Otak dan seberapa sering, atau apa yang perlu diriset di internet. Kalau ada yang belum jelas, saya tanyakan di sini.', 'agent.understood': 'Siap. Ini rencananya sejauh ini:\n{list}', 'agent.added': 'Ditambahkan ke rencana:\n{list}', 'agent.notUnderstood': 'Saya belum menemukan jenis konten atau permintaan riset di situ. Coba misalnya “2 ringkasan per hari soal leadership”.', 'agent.unclear': 'Maaf, saya belum menangkap maksudnya. Pilih salah satu opsi di bawah atau jawab dengan kata-kata sendiri.', 'agent.askUnknown': 'Saya belum paham “{part}”. Maksudnya apa? Tulis ulang, atau lewati saja.', 'agent.askVideo': '{rule}: jadwal video membuat satu video per proses, jadi pilihannya 1 per minggu atau 1 per hari. Mau yang mana?', 'agent.askTooMany': '{rule} lebih dari 50 per minggu, batas untuk satu jadwal. Berapa per minggu?', 'agent.askWeekday': 'Riset internet mingguan{about}: dijalankan hari apa? Sementara diatur ke Senin.', 'agent.askTopic': 'Topiknya tentang apa?\n{list}\nTulis topiknya, atau pakai seluruh Otak.', 'agent.ready': 'Semua sudah jelas: {n} jadwal siap. Anda masih bisa mengubahnya di rencana. Mulai sekarang?', 'agent.readyLanguage': 'Semua sudah jelas: {n} jadwal siap, dengan konten berbahasa {language}. Anda masih bisa mengubahnya di rencana. Mulai sekarang?', 'agent.askLanguage': 'Kontennya mau ditulis dalam bahasa apa?', 'agent.languageSet': 'Konten akan ditulis dalam {language}.', 'ins.language': 'Bahasa konten', 'ins.languagePending': 'Belum dipilih', 'agent.skipped': 'Oke, “{part}” saya lewati.', 'agent.removed': '{rule} dihapus.', 'agent.updated': 'Diperbarui: {rule}.', 'agent.topicSet': 'Topik diatur ke “{topic}”.', 'agent.wholeBrain': 'Oke, pakai seluruh Otak.', 'agent.reset': 'Mulai dari awal. Apa yang perlu dibuat?', 'agent.approving': 'Membuat jadwal…', 'agent.done': 'Selesai: {n} jadwal aktif dan langsung berjalan.', 'agent.partial': '{ok} dari {n} jadwal berhasil dibuat. Yang gagal masih ada di rencana: perbaiki, lalu setujui lagi.', 'agent.optSkip': 'Lewati', 'agent.optRemove': 'Hapus saja', 'agent.optWholeBrain': 'Seluruh Otak', 'agent.optApprove': 'Ya, mulai', 'agent.optReset': 'Mulai ulang', 'agent.optPerWeek': '1 per minggu', 'agent.optPerDay': '1 per hari', 'agent.opt50': '50 per minggu', 'agent.placeholder': 'Balas agen atau beri instruksi baru…', 'agent.send': 'Kirim', 'agent.hint': 'Enter untuk kirim, Shift+Enter untuk baris baru.', 'agent.planEmpty': 'Rencana terisi seiring Anda mengobrol dengan agen.', 'sch.nextVideo': 'Video berikutnya: {when}', 'sch.videoNow': 'Buat video sekarang', 'sch.videoStarted': 'Video mulai dibuat. Video tampil di Hasil selama proses berjalan.', 'sch.runStarted': 'Riset dimulai.', 'sch.deleted': 'Jadwal dihapus.', 'res.videoProgress': 'Merender… {n}%', 'res.videoNotReady': 'Video belum siap', 'res.videoFailed': 'Video gagal dibuat', 'res.videoFailedCopy': 'Proses render berhenti sebelum video selesai.', 'res.downloadVideo': 'Unduh video (MP4)', 'res.creditsUsed': 'Kredit terpakai', 'res.accepted': 'Disetujui.', 'res.rejected': 'Ditolak.', 'res.retry': 'Coba lagi', 'res.retrying': 'Mencoba lagi…', 'res.retried': 'Produksi dimulai lagi.', 'res.audioNotice': 'Diputar dengan suara perangkat ini, yang bisa berbeda antarperangkat. Unduh naskahnya untuk direkam atau dipakai di alat TTS.', 'res.fullScript': 'Lihat naskah lengkap', 'res.scriptTxt': 'Naskah (.txt)', 'res.scriptHint': 'Teks narasi, untuk direkam atau alat TTS', 'res.quiz': 'Kuis', 'res.quizPending': 'Kuisnya belum siap.', 'res.scormModule': 'Modul SCORM 1.2', 'res.scormMeta': 'Materi + {n} soal · nilai lulus {mastery}%', 'res.scormHint': 'Untuk LMS: materi, ditandai selesai setelah dibaca', 'res.scormQuizHint': 'Untuk LMS: materi dan kuis, melaporkan nilai dan lulus/tidak', 'res.scormReady': 'Paket SCORM terunduh. Unggah file .zip ke LMS Anda.', 'scorm.submit': 'Periksa jawaban', 'scorm.retry': 'Coba lagi', 'scorm.complete': 'Tandai selesai', 'scorm.completed': 'Selesai ✓', 'scorm.score': 'Nilai: {score}%.', 'scorm.passed': 'Lulus (nilai lulus {n}%).', 'scorm.failed': 'Belum lulus (nilai lulus {n}%).', 'scorm.answerAll': 'Jawab semua soal terlebih dahulu.', 'scorm.noLms': 'Dibuka di luar LMS: progres tidak disimpan.', 'audio.copy': 'Naskah narasi, dibacakan oleh perangkat ini.', 'audio.unsupported': 'Browser ini tidak bisa membacakan teks. Unduh naskahnya saja.', 'audio.play': 'Dengarkan', 'audio.pause': 'Jeda', 'audio.resume': 'Lanjutkan', 'audio.stop': 'Hentikan', 'audio.speed': 'Kecepatan', 'audio.ready': '{n} kalimat · tekan Dengarkan untuk mulai', 'audio.sentence': 'Kalimat {n} dari {total}', 'brain.deleted': '“{name}” dihapus.', // Panduan 'help.boxTitle': 'Cara kerja Express', 'help.boxBody': 'Empat langkah dari materi sampai konten jadi.', 'help.open': 'Lihat panduan', 'help.title': 'Cara kerja Learning Lab Express', 'help.subtitle': 'Otak → Perintah → Jadwal → Hasil', 'help.brainTitle': 'Otak', 'help.brainBody': 'Unggah materi pelatihan. Setelah pengetahuannya dibangun, AI membuat semua konten dari situ.', 'help.instructionTitle': 'Perintah', 'help.instructionBody': 'Tulis kebutuhan Anda dalam satu kalimat, misalnya “3 kuis per minggu dan 1 video per minggu soal leadership”. Periksa jadwalnya, lalu setujui.', 'help.scheduleTitle': 'Jadwal', 'help.scheduleBody': 'Jadwal berjalan sendiri. Jeda, ganti topik, atau produksi sekarang; masing-masing tetap dalam batas kreditnya.', 'help.resultsTitle': 'Hasil', 'help.resultsBody': 'Tinjau hasilnya, setujui atau tolak, lalu unduh: PDF, Markdown, PowerPoint, MP4, naskah audio, atau SCORM untuk LMS.', 'help.note': 'Konten dibuat AI: selalu tinjau sebelum dipakai. Kredit diambil dari ruang kerja organisasi Anda.', 'help.done': 'Mengerti', 'err.unreachable': 'Engine learning-lab tidak dapat dihubungi.', 'err.generic': 'Permintaan ke engine gagal.', 'err.credits': 'Kredit tidak cukup: butuh {required}, saldo {balance}.', 'err.creditsShort': 'Kredit tidak cukup.', 'err.docFailed': 'Dokumen gagal diproses.', 'err.timeout': 'Pemrosesan dokumen melebihi batas waktu.', 'err.researchFailed': 'Riset gagal dijalankan ({code}).', } const dictionaries: Record> = { en, id } const STORAGE = 'lle-lang' function initial(): Lang { try { const saved = localStorage.getItem(STORAGE) if (saved === 'en' || saved === 'id') return saved } catch { /* storage blocked: fall back to the default */ } return 'en' } let lang: Lang = initial() // The language of the text being built right now, when it differs from the UI (see inLang). let override: Lang | null = null const listeners = new Set<() => void>() if (typeof document !== 'undefined') document.documentElement.lang = lang export const getLang = () => lang export function setLang(next: Lang) { if (next === lang) return lang = next if (typeof document !== 'undefined') document.documentElement.lang = next try { localStorage.setItem(STORAGE, next) } catch { /* per-device preference only */ } listeners.forEach(l => l()) } export function t(key: Key, vars?: Record): string { const text = dictionaries[override ?? lang][key] ?? en[key] return vars ? text.replace(/\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m)) : text } /** Runs fn with t() in another language, e.g. the agent answering in the language the user writes in. */ export function inLang(l: Lang, fn: () => T): T { const previous = override override = l try { return fn() } finally { override = previous } } /** Re-renders the component when the language changes; returns the active language. */ export function useLang(): Lang { return useSyncExternalStore(cb => { listeners.add(cb); return () => listeners.delete(cb) }, getLang) } /** BCP 47 locale for dates and numbers. */ export const locale = () => (lang === 'id' ? 'id-ID' : 'en-US')