2026-09-15 03:20:42 +00:00
import { createContext , useContext , useEffect , useMemo , useState , type ReactNode } from "react" ;
export const LOCALES = [ "zh-Hant" , "zh-Hans" , "en" , "ja" ] as const ;
export type Locale = ( typeof LOCALES ) [ number ] ;
export const LOCALE_OPTIONS : { id : Locale ; native : string } [ ] = [
{ id : "zh-Hant" , native : "繁體中文" } ,
{ id : "zh-Hans" , native : "简体中文" } ,
{ id : "en" , native : "English" } ,
{ id : "ja" , native : "日本語" } ,
] ;
2026-09-15 05:20:44 +00:00
const STORAGE_KEY = "lazyboy.locale" ;
2026-09-15 03:20:42 +00:00
const zhHant = {
2026-09-15 05:20:44 +00:00
handoverTitle : "需要你操作" ,
handoverHint : "請直接在下方原網站輸入密碼或驗證碼,不要貼到聊天訊息。" ,
handoverLocal : "請在本機已開啟的工具瀏覽器操作。" ,
handoverDone : "完成並繼續" ,
handoverReturning : "正在交回操作權…" ,
executionDetails : "執行詳情" ,
2026-09-15 03:20:42 +00:00
language : "語言" ,
settings : "設定" ,
2026-09-15 05:20:44 +00:00
about : "關於" ,
general : "一般" ,
appearance : "外觀" ,
theme : "主題" ,
themeSystem : "跟隨系統" ,
themeLight : "淺色" ,
themeDark : "深色" ,
updates : "更新" ,
copyVersionInfo : "複製版本資訊" ,
copied : "已複製" ,
copyright : "Copyright © 2026 Daniel Wang" ,
settingsLabel : "LazyBoy 設定" ,
workspaceDetail : "Agent 與對話都留在這台機器。" ,
versionLine : "版本 {version}" ,
computerUpdateCopy : "更新助手共用的電腦。檔案與登入會保留。" ,
updateComputer : "更新電腦" ,
restartComputer : "重啟電腦" ,
restartComputerCopy : "電腦卡住時重啟。瀏覽器登入會保留。" ,
appComputer : "LazyBoy 的電腦" ,
computerBusyCopy : "有 Agent 正在工作。現在操作會中斷它。" ,
localBuild : "本機版本" ,
2026-09-15 03:20:42 +00:00
close : "關閉" ,
addAgent : "新增 Agent" ,
deleteAgent : "刪除 Agent" ,
deleteAgentConfirm : "刪除 {name}?對話與記憶會一併刪掉,無法復原。" ,
search : "搜尋" ,
agentNamePlaceholder : "Agent 名稱" ,
agents : "Agent" ,
noAgents : "還沒有 Agent" ,
noMatchingAgents : "沒有符合的 Agent" ,
queuedWork : "等待開始…" ,
activityUnknown : "暫時無法確認工作狀態,正在重新連線。" ,
agentWorking : "工作中" ,
newChat : "新對話" ,
localWorkspace : "本機工作區" ,
pickAgent : "選擇 Agent" ,
workTools : "工作工具" ,
stop : "停止" ,
computer : "電腦" ,
startWorkWith : "開始跟 {name} 工作" ,
startWorkHint : "傳一則訊息,或打開右邊的電腦看它動手做。" ,
workingWith : "{name} 正在工作…" ,
moreActions : "更多動作" ,
messageTo : "傳訊息給 {name}" ,
pickAgentFirst : "先選一個 Agent" ,
send : "送出" ,
collapse : "收合" ,
computerOf : "{name} 的電腦" ,
enlarge : "放大" ,
loginHint : "在這台機器人自己的瀏覽器登入。Session 留在它的電腦,不要把密碼打在聊天裡。" ,
chat : "聊天" ,
mainMenu : "主選單" ,
restart : "重啟" ,
update : "更新" ,
restartTitle : "重啟這台電腦" ,
updateTitle : "重建映像並在有更新時重開" ,
restartConfirm : "重啟會暫時中斷桌面與正在跑的操作。瀏覽器登入會保留。確定重啟?" ,
updateConfirm :
"更新會依目前的 Dockerfile 重建這台電腦的映像。登入與 /workspace 會留在 volume。若映像有變就會重開桌面, 可能需要幾分鐘。確定更新? " ,
computerStarting : "電腦啟動中…" ,
computerRestarting : "正在重啟電腦…" ,
computerUpdating : "正在更新電腦映像…" ,
computerRunning : "執行中" ,
computerUpToDate : "映像已是最新" ,
computerNotReady : "電腦未就緒" ,
computerNotStarted : "尚未啟動" ,
computerOff : "未啟動" ,
thinking : "思考中" ,
needsReply : "需要回覆" ,
stepComputer : "操作電腦" ,
stepSearch : "搜尋中" ,
stepFetch : "讀取網頁" ,
stepBrowser : "操作瀏覽器" ,
stepBox : "在電腦上執行" ,
stepShell : "執行指令" ,
stepWrite : "改檔案" ,
stepRead : "讀檔" ,
stepWorking : "處理中" ,
} as const ;
export type MessageKey = keyof typeof zhHant ;
export type Messages = Record < MessageKey , string > ;
const zhHans : Messages = {
2026-09-15 05:20:44 +00:00
handoverTitle : "需要你操作" ,
handoverHint : "请直接在下方原网站输入密码或验证码,不要贴到聊天消息。" ,
handoverLocal : "请在本机已打开的工具浏览器操作。" ,
handoverDone : "完成并继续" ,
handoverReturning : "正在交回操作权…" ,
executionDetails : "执行详情" ,
2026-09-15 03:20:42 +00:00
language : "语言" ,
settings : "设置" ,
2026-09-15 05:20:44 +00:00
about : "关于" ,
general : "一般" ,
appearance : "外观" ,
theme : "主题" ,
themeSystem : "跟随系统" ,
themeLight : "浅色" ,
themeDark : "深色" ,
updates : "更新" ,
copyVersionInfo : "复制版本信息" ,
copied : "已复制" ,
copyright : "Copyright © 2026 Daniel Wang" ,
settingsLabel : "LazyBoy 设置" ,
workspaceDetail : "Agent 和对话都留在这台机器。" ,
versionLine : "版本 {version}" ,
computerUpdateCopy : "更新助手共用的电脑。文件和登录会保留。" ,
updateComputer : "更新电脑" ,
restartComputer : "重启电脑" ,
restartComputerCopy : "电脑卡住时重启。浏览器登录会保留。" ,
appComputer : "LazyBoy 的电脑" ,
computerBusyCopy : "有 Agent 正在工作。现在操作会中断它。" ,
localBuild : "本地版本" ,
2026-09-15 03:20:42 +00:00
close : "关闭" ,
addAgent : "新增 Agent" ,
deleteAgent : "删除 Agent" ,
deleteAgentConfirm : "删除 {name}?对话和记忆会一并删掉,无法恢复。" ,
search : "搜索" ,
agentNamePlaceholder : "Agent 名称" ,
agents : "Agent" ,
noAgents : "还没有 Agent" ,
noMatchingAgents : "没有符合的 Agent" ,
queuedWork : "等待开始…" ,
activityUnknown : "暂时无法确认工作状态,正在重新连接。" ,
agentWorking : "工作中" ,
newChat : "新对话" ,
localWorkspace : "本地工作区" ,
pickAgent : "选择 Agent" ,
workTools : "工作工具" ,
stop : "停止" ,
computer : "电脑" ,
startWorkWith : "开始和 {name} 工作" ,
startWorkHint : "发一条消息,或打开右边的电脑看它动手做。" ,
workingWith : "{name} 正在工作…" ,
moreActions : "更多操作" ,
messageTo : "发消息给 {name}" ,
pickAgentFirst : "先选一个 Agent" ,
send : "发送" ,
collapse : "收起" ,
computerOf : "{name} 的电脑" ,
enlarge : "放大" ,
loginHint : "在这台机器人自己的浏览器登录。Session 留在它的电脑,不要把密码打在聊天里。" ,
chat : "聊天" ,
mainMenu : "主菜单" ,
restart : "重启" ,
update : "更新" ,
restartTitle : "重启这台电脑" ,
updateTitle : "重建镜像并在有更新时重开" ,
restartConfirm : "重启会暂时中断桌面和正在跑的操作。浏览器登录会保留。确定重启?" ,
updateConfirm :
"更新会按当前的 Dockerfile 重建这台电脑的镜像。登录和 /workspace 会留在 volume。若镜像有变就会重开桌面, 可能需要几分钟。确定更新? " ,
computerStarting : "电脑启动中…" ,
computerRestarting : "正在重启电脑…" ,
computerUpdating : "正在更新电脑镜像…" ,
computerRunning : "运行中" ,
computerUpToDate : "镜像已是最新" ,
computerNotReady : "电脑未就绪" ,
computerNotStarted : "尚未启动" ,
computerOff : "未启动" ,
thinking : "思考中" ,
needsReply : "需要回复" ,
stepComputer : "操作电脑" ,
stepSearch : "搜索中" ,
stepFetch : "读取网页" ,
stepBrowser : "操作浏览器" ,
stepBox : "在电脑上执行" ,
stepShell : "执行命令" ,
stepWrite : "改文件" ,
stepRead : "读文件" ,
stepWorking : "处理中" ,
} ;
const en : Messages = {
2026-09-15 05:20:44 +00:00
handoverTitle : "Your action is needed" ,
handoverHint : "Enter passwords or verification codes on the original website below, not in chat." ,
handoverLocal : "Use the tool browser already open on your computer." ,
handoverDone : "Done, continue" ,
handoverReturning : "Returning control…" ,
executionDetails : "Execution details" ,
2026-09-15 03:20:42 +00:00
language : "Language" ,
settings : "Settings" ,
2026-09-15 05:20:44 +00:00
about : "About" ,
general : "General" ,
appearance : "Appearance" ,
theme : "Theme" ,
themeSystem : "Follow System" ,
themeLight : "Light" ,
themeDark : "Dark" ,
updates : "Updates" ,
copyVersionInfo : "Copy version info" ,
copied : "Copied" ,
copyright : "Copyright © 2026 Daniel Wang" ,
settingsLabel : "LazyBoy settings" ,
workspaceDetail : "Agents and chats stay on this machine." ,
versionLine : "Version {version}" ,
computerUpdateCopy : "Updates the computer your assistants share. Your files and logins stay." ,
updateComputer : "Update LazyBoy's Computer" ,
restartComputer : "Restart LazyBoy's Computer" ,
restartComputerCopy : "Restart if the computer gets stuck. Browser logins are kept." ,
appComputer : "LazyBoy's Computer" ,
computerBusyCopy : "An agent is working. This will interrupt it." ,
localBuild : "Local build" ,
2026-09-15 03:20:42 +00:00
close : "Close" ,
addAgent : "New agent" ,
deleteAgent : "Delete agent" ,
deleteAgentConfirm : "Delete {name}? Its chats and memory will be removed. This cannot be undone." ,
search : "Search" ,
agentNamePlaceholder : "Agent name" ,
agents : "Agents" ,
noAgents : "No agents yet" ,
noMatchingAgents : "No matching agents" ,
queuedWork : "Queued…" ,
activityUnknown : "Work status is unavailable. Reconnecting." ,
agentWorking : "Working" ,
newChat : "New chat" ,
localWorkspace : "Local workspace" ,
pickAgent : "Choose an agent" ,
workTools : "Work tools" ,
stop : "Stop" ,
computer : "Computer" ,
startWorkWith : "Start working with {name}" ,
startWorkHint : "Send a message, or open the computer on the right to watch it work." ,
workingWith : "{name} is working…" ,
moreActions : "More actions" ,
messageTo : "Message {name}" ,
pickAgentFirst : "Choose an agent first" ,
send : "Send" ,
collapse : "Collapse" ,
computerOf : "{name}'s computer" ,
enlarge : "Expand" ,
loginHint : "Sign in inside this agent's own browser. The session stays on its computer — don't type passwords in chat." ,
chat : "Chat" ,
mainMenu : "Main menu" ,
restart : "Restart" ,
update : "Update" ,
restartTitle : "Restart this computer" ,
updateTitle : "Rebuild the image and reopen if it changed" ,
restartConfirm : "Restart will briefly interrupt the desktop and any running work. Browser logins are kept. Restart now?" ,
updateConfirm :
"Update rebuilds this computer's image from the current Dockerfile. Logins and /workspace stay on the volume. If the image changed, the desktop will reopen. This can take several minutes. Update now?" ,
computerStarting : "Starting computer…" ,
computerRestarting : "Restarting computer…" ,
computerUpdating : "Updating computer image…" ,
computerRunning : "Running" ,
computerUpToDate : "Image is up to date" ,
computerNotReady : "Computer is not ready" ,
computerNotStarted : "Not started yet" ,
computerOff : "Off" ,
thinking : "Thinking" ,
needsReply : "Needs a reply" ,
stepComputer : "Using the computer" ,
stepSearch : "Searching" ,
stepFetch : "Reading a page" ,
stepBrowser : "Using the browser" ,
stepBox : "Running on the computer" ,
stepShell : "Running a command" ,
stepWrite : "Editing a file" ,
stepRead : "Reading a file" ,
stepWorking : "Working" ,
} ;
const ja : Messages = {
2026-09-15 05:20:44 +00:00
handoverTitle : "操作が必要です" ,
handoverHint : "パスワードや認証コードは下の元のサイトに入力してください。チャットには入力しないでください。" ,
handoverLocal : "既に開いているツールブラウザーで操作してください。" ,
handoverDone : "完了して続行" ,
handoverReturning : "操作を引き継いでいます…" ,
executionDetails : "実行の詳細" ,
2026-09-15 03:20:42 +00:00
language : "言語" ,
settings : "設定" ,
2026-09-15 05:20:44 +00:00
about : "バージョン情報" ,
general : "一般" ,
appearance : "外観" ,
theme : "テーマ" ,
themeSystem : "システムに合わせる" ,
themeLight : "ライト" ,
themeDark : "ダーク" ,
updates : "アップデート" ,
copyVersionInfo : "バージョン情報をコピー" ,
copied : "コピーしました" ,
copyright : "Copyright © 2026 Daniel Wang" ,
settingsLabel : "LazyBoy の設定" ,
workspaceDetail : "Agent と会話はこのマシンに残ります。" ,
versionLine : "バージョン {version}" ,
computerUpdateCopy : "アシスタントが共有するコンピュータを更新します。ファイルとログインは残ります。" ,
updateComputer : "コンピュータを更新" ,
restartComputer : "コンピュータを再起動" ,
restartComputerCopy : "コンピュータが止まったときに再起動します。ブラウザのログインは残ります。" ,
appComputer : "LazyBoy のコンピュータ" ,
computerBusyCopy : "Agent が作業中です。今操作すると中断されます。" ,
localBuild : "ローカルビルド" ,
2026-09-15 03:20:42 +00:00
close : "閉じる" ,
addAgent : "Agent を追加" ,
deleteAgent : "Agent を削除" ,
deleteAgentConfirm : "{name} を削除しますか?会話と記憶も消えます。元に戻せません。" ,
search : "検索" ,
agentNamePlaceholder : "Agent 名" ,
agents : "Agent" ,
noAgents : "まだ Agent がありません" ,
noMatchingAgents : "一致する Agent がありません" ,
queuedWork : "開始待ち…" ,
activityUnknown : "作業状態を確認できません。再接続しています。" ,
agentWorking : "作業中" ,
newChat : "新しい会話" ,
localWorkspace : "ローカルワークスペース" ,
pickAgent : "Agent を選ぶ" ,
workTools : "作業ツール" ,
stop : "停止" ,
computer : "コンピュータ" ,
startWorkWith : "{name} と作業を始める" ,
startWorkHint : "メッセージを送るか、右のコンピュータを開いて作業を見てください。" ,
workingWith : "{name} が作業中…" ,
moreActions : "その他" ,
messageTo : "{name} にメッセージ" ,
pickAgentFirst : "先に Agent を選んでください" ,
send : "送信" ,
collapse : "折りたたむ" ,
computerOf : "{name} のコンピュータ" ,
enlarge : "拡大" ,
loginHint : "このエージェント自身のブラウザでログインしてください。セッションはそのコンピュータに残ります。パスワードをチャットに入力しないでください。" ,
chat : "チャット" ,
mainMenu : "メインメニュー" ,
restart : "再起動" ,
update : "更新" ,
restartTitle : "このコンピュータを再起動" ,
updateTitle : "イメージを再構築し、変更があれば開き直す" ,
restartConfirm : "再起動するとデスクトップと実行中の操作が一時的に中断されます。ブラウザのログインは残ります。再起動しますか?" ,
updateConfirm :
"更新すると現在の Dockerfile からこのコンピュータのイメージを再構築します。ログインと /workspace は volume に残ります。イメージが変わった場合はデスクトップを開き直します。数分かかることがあります。更新しますか?" ,
computerStarting : "コンピュータを起動中…" ,
computerRestarting : "コンピュータを再起動中…" ,
computerUpdating : "コンピュータのイメージを更新中…" ,
computerRunning : "実行中" ,
computerUpToDate : "イメージは最新です" ,
computerNotReady : "コンピュータの準備ができていません" ,
computerNotStarted : "まだ起動していません" ,
computerOff : "停止中" ,
thinking : "考え中" ,
needsReply : "返信が必要です" ,
stepComputer : "コンピュータを操作中" ,
stepSearch : "検索中" ,
stepFetch : "ページを読み取り中" ,
stepBrowser : "ブラウザを操作中" ,
stepBox : "コンピュータで実行中" ,
stepShell : "コマンドを実行中" ,
stepWrite : "ファイルを編集中" ,
stepRead : "ファイルを読み取り中" ,
stepWorking : "処理中" ,
} ;
export const MESSAGES : Record < Locale , Messages > = {
"zh-Hant" : zhHant ,
"zh-Hans" : zhHans ,
en ,
ja ,
} ;
export function isLocale ( value : string | null | undefined ) : value is Locale {
return LOCALES . includes ( value as Locale ) ;
}
export function detectLocale ( ) : Locale {
try {
2026-09-15 05:20:44 +00:00
const saved = localStorage . getItem ( STORAGE_KEY ) || localStorage . getItem ( "grokboy.locale" ) ;
2026-09-15 03:20:42 +00:00
if ( isLocale ( saved ) ) return saved ;
} catch {
/* ignore */
}
const nav = typeof navigator === "undefined" ? "" : navigator . language || "" ;
const lower = nav . toLowerCase ( ) ;
if ( lower . startsWith ( "ja" ) ) return "ja" ;
if ( lower . startsWith ( "en" ) ) return "en" ;
if ( lower . startsWith ( "zh" ) ) {
if ( lower . includes ( "cn" ) || lower . includes ( "sg" ) || lower . includes ( "hans" ) ) return "zh-Hans" ;
return "zh-Hant" ;
}
return "zh-Hant" ;
}
export function formatMessage ( template : string , vars? : Record < string , string > ) : string {
if ( ! vars ) return template ;
return template . replace ( /\{(\w+)\}/g , ( _ , key : string ) = > vars [ key ] ? ? ` { ${ key } } ` ) ;
}
type I18nValue = {
locale : Locale ;
setLocale : ( locale : Locale ) = > void ;
t : Messages ;
format : ( key : MessageKey , vars? : Record < string , string > ) = > string ;
} ;
const I18nContext = createContext < I18nValue | null > ( null ) ;
export function I18nProvider ( { children } : { children : ReactNode } ) {
const [ locale , setLocaleState ] = useState < Locale > ( detectLocale ) ;
useEffect ( ( ) = > {
document . documentElement . lang = locale ;
try {
localStorage . setItem ( STORAGE_KEY , locale ) ;
} catch {
/* ignore */
}
} , [ locale ] ) ;
const value = useMemo < I18nValue > ( ( ) = > {
const t = MESSAGES [ locale ] ;
return {
locale ,
setLocale : setLocaleState ,
t ,
format : ( key , vars ) = > formatMessage ( t [ key ] , vars ) ,
} ;
} , [ locale ] ) ;
return < I18nContext.Provider value = { value } > { children } < / I18nContext.Provider > ;
}
export function useI18n ( ) : I18nValue {
const ctx = useContext ( I18nContext ) ;
if ( ! ctx ) throw new Error ( "useI18n must be used within I18nProvider" ) ;
return ctx ;
}