Files
learning-lab-express/src/i18n.ts

883 lines
44 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<Key, string> = {
'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<Lang, Record<Key, string>> = { 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, string | number>): 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<T>(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')