A small browser tool for Rock RMS administrators that translates a Rock entity Key (its IdHash) to its integer Id, and back again. Select a value on any page of your Rock instance, trigger the tool, and the translated value is copied to your clipboard and shown in a quick popup.
It works while you are logged into Rock: the request is authenticated by your existing session cookie, so there is no API key to configure and no secret stored anywhere.
- Reads the text you have selected (or prompts you for a value).
- Sends it to Rock's Lava
RenderTemplateAPI on the site you are currently on. - Translates a Key to an Id (the common case), or an Id to a Key.
- Copies the result to your clipboard and shows it in an alert.
The default direction is Key to Id; the reverse is also supported.
Supports Chrome, Edge, and Firefox. Safari is not supported.
Chrome / Edge
- Go to
chrome://extensions(oredge://extensions). - Enable Developer mode.
- Click Load unpacked and select the
extension/folder.
Firefox
- In the
extension/folder, usemanifest.firefox.jsonas the manifest (rename it tomanifest.jsonfor a local load, or build a separate Firefox package). - Go to
about:debugging> This Firefox > Load Temporary Add-on and select the manifest. (A temporary add-on is removed on restart; a permanent install requires AMO signing.)
After loading, trigger it three ways: the toolbar button, a right-click Translate Rock Key / Id on selected text, or the keyboard shortcut (Alt+Shift+K by default).
Prefer not to install the extension? The bookmarklet has the same behavior. Nothing to build or run; it is ready to copy.
- Open
bookmarklet/bookmarklet.url.txtand copy its entire contents (or copy from the box below). - Create a new bookmark on your bookmarks bar.
- Paste the copied text as the bookmark's URL and save.
- On any Rock page, select a Key or Id (including inside a modal) and click the bookmark. With nothing selected, it prompts for a value.
Bookmarklet URL (click to expand, then copy)
javascript:(function()%7B%0A%2F%2F%20Rock%20Key%20Translator%3A%20canonical%20shared%20core.%0A%2F%2F%0A%2F%2F%20This%20single%20function%20implements%20the%20whole%20behavior%20(Sections%205-10%20of%0A%2F%2F%20REQUIREMENTS.md)%3A%20obtain%20the%20input%2C%20compose%20the%20Lava%20template%2C%20call%20the%0A%2F%2F%20RenderTemplate%20API%2C%20parse%20the%20response%2C%20copy%20the%20result%2C%20and%20report%20it.%0A%2F%2F%0A%2F%2F%20It%20is%20intentionally%20SELF-CONTAINED%3A%20it%20references%20nothing%20in%20module%20scope%2C%20so%0A%2F%2F%20it%20can%20be%20(a)%20injected%20verbatim%20by%20the%20extension%20via%0A%2F%2F%20chrome.scripting.executeScript(%7B%20func%3A%20rockKeyTranslate%20%7D)%20and%20(b)%20inlined%0A%2F%2F%20into%20the%20bookmarklet.%20Keep%20all%20helpers%20as%20inner%20functions.%0A%2F%2F%0A%2F%2F%20%60preselectedText%60%20is%20the%20text%20supplied%20by%20a%20trigger%20that%20already%20knows%20the%0A%2F%2F%20selection%20(the%20extension's%20context-menu%20click%20passes%20info.selectionText).%0A%2F%2F%20When%20it%20is%20null%2Fempty%2C%20the%20core%20reads%20the%20page%20selection%20itself%2C%20then%20prompts.%0A%0Afunction%20rockKeyTranslate(preselectedText)%20%7B%0A%20%20const%20ENDPOINT%20%3D%20%22%2Fapi%2FLava%2FRenderTemplate%22%3B%0A%0A%20%20%2F%2F%20---%20template%20---------------------------------------------------------%0A%0A%20%20%2F%2F%20Escape%20the%20user%20value%20so%20it%20cannot%20break%20out%20of%20the%20Lava%20string%20literal%20or%0A%20%20%2F%2F%20inject%20Lava%20control%20sequences%20(it%20runs%20server-side%20as%20the%20logged-in%20user).%0A%20%20function%20escapeForLava(value)%20%7B%0A%20%20%20%20return%20String(value)%0A%20%20%20%20%20%20.replace(%2F'%2Fg%2C%20%22%5C%5C'%22)%0A%20%20%20%20%20%20.replace(%2F%5C%7B%5C%7B%7C%5C%7D%5C%7D%7C%5C%7B%25%7C%25%5C%7D%2Fg%2C%20%22%22)%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20The%20default%20Key%20%3C-%3E%20Id%20translator.%20The%20input%20is%20injected%20as%20a%20quoted%20string%0A%20%20%2F%2F%20literal.%20FromIdHash%20returns%20the%20same%20integer%20when%20given%20an%20Id%2C%20so%0A%20%20%2F%2F%20%60valId%20%3D%3D%20input%60%20detects%20%22input%20was%20an%20Id%22%20(encode%20it)%20versus%20%22input%20was%20a%0A%20%20%2F%2F%20Key%22%20(decode%20it).%20Validated%20against%20Rock's%20RenderTemplate%20endpoint.%0A%20%20function%20buildTemplate(input)%20%7B%0A%20%20%20%20const%20v%20%3D%20escapeForLava(input)%3B%0A%20%20%20%20return%20%5B%0A%20%20%20%20%20%20%22%7B%25%20assign%20input%20%3D%20'%22%20%2B%20v%20%2B%20%22'%20%7C%20Trim%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%7B%25%20assign%20return%20%3D%20''%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%7B%25%20if%20input%20%3D%3D%20empty%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%7B%25%20assign%20return%20%3D%20return%20%7C%20AddToDictionary%3A'ErrCode'%2C1%20%7C%20AddToDictionary%3A'ErrMsg'%2C'No%20value%20was%20provided%20to%20translate.'%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%7B%25%20else%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%7B%25%20assign%20valId%20%3D%20input%20%7C%20FromIdHash%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%7B%25%20if%20valId%20%3D%3D%20input%20%25%7D%20%2F%2F-%20input%20is%20an%20Id%3B%20encode%20it%20to%20a%20Key.%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20assign%20asInt%20%3D%20input%20%7C%20AsInteger%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20if%20asInt%20%3D%3D%20empty%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20assign%20return%20%3D%20return%20%7C%20AddToDictionary%3A'ErrCode'%2C2%20%7C%20AddToDictionary%3A'ErrMsg'%2C'That%20value%20is%20not%20a%20valid%20Key%20or%20Id.'%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20else%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20assign%20key%20%3D%20asInt%20%7C%20ToIdHash%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20if%20key%20%3D%3D%20empty%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%20%20%7B%25%20assign%20return%20%3D%20return%20%7C%20AddToDictionary%3A'ErrCode'%2C3%20%7C%20AddToDictionary%3A'ErrMsg'%2C'The%20value%20could%20not%20be%20translated.'%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20else%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%20%20%7B%25%20assign%20return%20%3D%20return%20%7C%20AddToDictionary%3A'ErrCode'%2C0%20%7C%20AddToDictionary%3A'Msg'%2C'The%20Key%20for%20that%20Id%20is%20'%20%7C%20AddToDictionary%3A'Val'%2Ckey%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20endif%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20endif%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%7B%25%20else%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20if%20valId%20%3D%3D%20empty%20or%20valId%20%3D%3D%20null%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20assign%20return%20%3D%20return%20%7C%20AddToDictionary%3A'ErrCode'%2C4%20%7C%20AddToDictionary%3A'ErrMsg'%2C'The%20value%20could%20not%20be%20translated.'%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20else%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%20%20%7B%25%20assign%20return%20%3D%20return%20%7C%20AddToDictionary%3A'ErrCode'%2C0%20%7C%20AddToDictionary%3A'Msg'%2C'That%20Key%20translates%20to%20the%20Id%20'%20%7C%20AddToDictionary%3A'Val'%2CvalId%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%20%20%7B%25%20endif%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%20%20%7B%25%20endif%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%7B%25%20endif%20%25%7D%22%2C%0A%20%20%20%20%20%20%22%7B%7B%20return%20%7C%20ToJSON%20%7D%7D%22%0A%20%20%20%20%5D.join(%22%5Cn%22)%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20---%20input%20acquisition%20------------------------------------------------%0A%0A%20%20%2F%2F%20Read%20the%20selection%20from%20a%20single%20window%3A%20its%20active%20form%20field%20(whose%20text%0A%20%20%2F%2F%20window.getSelection()%20does%20not%20always%20cover)%20or%20its%20document%20selection.%0A%20%20%2F%2F%20Wrapped%20in%20try%2Fcatch%20because%20cross-origin%20frames%20are%20not%20readable.%0A%20%20function%20selectionFromWindow(win)%20%7B%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20const%20el%20%3D%20win.document.activeElement%3B%0A%20%20%20%20%20%20if%20(%0A%20%20%20%20%20%20%20%20el%20%26%26%0A%20%20%20%20%20%20%20%20(el.tagName%20%3D%3D%3D%20%22INPUT%22%20%7C%7C%20el.tagName%20%3D%3D%3D%20%22TEXTAREA%22)%20%26%26%0A%20%20%20%20%20%20%20%20typeof%20el.selectionStart%20%3D%3D%3D%20%22number%22%20%26%26%0A%20%20%20%20%20%20%20%20el.selectionStart%20!%3D%3D%20el.selectionEnd%0A%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20return%20el.value.substring(el.selectionStart%2C%20el.selectionEnd)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20return%20win.getSelection%20%3F%20win.getSelection().toString()%20%3A%20%22%22%3B%0A%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20return%20%22%22%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%0A%20%20%2F%2F%20Read%20the%20current%20document%20first%2C%20then%20same-origin%20child%20frames.%20Rock%20renders%0A%20%20%2F%2F%20modal%20dialogs%20inside%20%23modal-popup_iframe%2C%20so%20a%20value%20highlighted%20in%20a%20modal%0A%20%20%2F%2F%20lives%20in%20a%20child%20frame%2C%20not%20the%20top%20document.%20The%20toolbar%2Fkeyboard%20triggers%0A%20%20%2F%2F%20inject%20into%20the%20top%20frame%20only%2C%20so%20without%20this%20they%20would%20miss%20it.%0A%20%20function%20readSelection()%20%7B%0A%20%20%20%20let%20s%20%3D%20selectionFromWindow(window)%3B%0A%20%20%20%20if%20(s%20%26%26%20s.trim())%20return%20s%3B%0A%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20window.frames.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20s%20%3D%20selectionFromWindow(window.frames%5Bi%5D)%3B%0A%20%20%20%20%20%20if%20(s%20%26%26%20s.trim())%20return%20s%3B%0A%20%20%20%20%7D%0A%20%20%20%20return%20%22%22%3B%0A%20%20%7D%0A%0A%20%20function%20resolveInput()%20%7B%0A%20%20%20%20const%20raw%20%3D%20(preselectedText%20!%3D%20null%20%26%26%20String(preselectedText)%20!%3D%3D%20%22%22)%0A%20%20%20%20%20%20%3F%20String(preselectedText)%0A%20%20%20%20%20%20%3A%20readSelection()%3B%0A%20%20%20%20const%20value%20%3D%20(raw%20%7C%7C%20%22%22).trim()%3B%0A%0A%20%20%20%20%2F%2F%20A%20valid%20selection%20is%20non-empty%20with%20no%20internal%20whitespace.%0A%20%20%20%20if%20(value%20%26%26%20!%2F%5Cs%2F.test(value))%20%7B%0A%20%20%20%20%20%20return%20value%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20Otherwise%20prompt.%20Cancel%20or%20empty%20aborts%20silently.%0A%20%20%20%20const%20entered%20%3D%20window.prompt(%22Enter%20a%20Rock%20Key%20(IdHash)%20or%20Id%20to%20translate%3A%22)%3B%0A%20%20%20%20if%20(entered%20%3D%3D%20null)%20return%20null%3B%0A%20%20%20%20const%20trimmed%20%3D%20entered.trim()%3B%0A%20%20%20%20return%20trimmed%20%3F%20trimmed%20%3A%20null%3B%0A%20%20%7D%0A%0A%20%20%2F%2F%20---%20clipboard%20--------------------------------------------------------%0A%0A%20%20async%20function%20copyToClipboard(text)%20%7B%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20if%20(navigator.clipboard%20%26%26%20navigator.clipboard.writeText)%20%7B%0A%20%20%20%20%20%20%20%20await%20navigator.clipboard.writeText(text)%3B%0A%20%20%20%20%20%20%20%20return%20true%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%2F%2F%20fall%20through%20to%20the%20legacy%20path%0A%20%20%20%20%7D%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20const%20ta%20%3D%20document.createElement(%22textarea%22)%3B%0A%20%20%20%20%20%20ta.value%20%3D%20text%3B%0A%20%20%20%20%20%20ta.setAttribute(%22readonly%22%2C%20%22%22)%3B%0A%20%20%20%20%20%20ta.style.position%20%3D%20%22fixed%22%3B%0A%20%20%20%20%20%20ta.style.top%20%3D%20%22-1000px%22%3B%0A%20%20%20%20%20%20ta.style.opacity%20%3D%20%220%22%3B%0A%20%20%20%20%20%20document.body.appendChild(ta)%3B%0A%20%20%20%20%20%20ta.focus()%3B%0A%20%20%20%20%20%20ta.select()%3B%0A%20%20%20%20%20%20const%20ok%20%3D%20document.execCommand(%22copy%22)%3B%0A%20%20%20%20%20%20document.body.removeChild(ta)%3B%0A%20%20%20%20%20%20return%20ok%3B%0A%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%0A%20%20%2F%2F%20---%20HTTP%20status%20messages%20--------------------------------------------%0A%0A%20%20function%20messageForStatus(status%2C%20statusText)%20%7B%0A%20%20%20%20switch%20(status)%20%7B%0A%20%20%20%20%20%20case%20401%3A%0A%20%20%20%20%20%20%20%20return%20%22Not%20Authorized.%20Your%20Rock%20session%20may%20have%20expired%3B%20please%20log%20in%20again.%22%3B%0A%20%20%20%20%20%20case%20403%3A%0A%20%20%20%20%20%20%20%20return%20%22Forbidden.%20Your%20account%20does%20not%20have%20permission%20to%20render%20Lava%20via%20the%20API.%22%3B%0A%20%20%20%20%20%20case%20404%3A%0A%20%20%20%20%20%20%20%20return%20%22Endpoint%20not%20found.%20%22%20%2B%20ENDPOINT%20%2B%20%22%20is%20unavailable%20on%20this%20server.%22%3B%0A%20%20%20%20%20%20case%20405%3A%0A%20%20%20%20%20%20%20%20return%20%22Method%20Not%20Allowed.%20The%20endpoint%20did%20not%20accept%20the%20request%20method.%22%3B%0A%20%20%20%20%20%20case%20429%3A%0A%20%20%20%20%20%20%20%20return%20%22Too%20Many%20Requests.%20Please%20wait%20a%20moment%20and%20try%20again.%22%3B%0A%20%20%20%20%20%20default%3A%0A%20%20%20%20%20%20%20%20if%20(status%20%3E%3D%20500%20%26%26%20status%20%3C%3D%20599)%20%7B%0A%20%20%20%20%20%20%20%20%20%20return%20%22Rock%20encountered%20a%20server%20error%20while%20rendering%20the%20template.%22%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20return%20%22Unexpected%20response%20from%20Rock%20(HTTP%20%22%20%2B%20status%20%2B%0A%20%20%20%20%20%20%20%20%20%20(statusText%20%3F%20%22%20%22%20%2B%20statusText%20%3A%20%22%22)%20%2B%20%22).%22%3B%0A%20%20%20%20%7D%0A%20%20%7D%0A%0A%20%20%2F%2F%20---%20main%20flow%20--------------------------------------------------------%0A%0A%20%20return%20(async%20function%20run()%20%7B%0A%20%20%20%20const%20input%20%3D%20resolveInput()%3B%0A%20%20%20%20if%20(input%20%3D%3D%20null)%20return%3B%20%2F%2F%20aborted%20silently%0A%0A%20%20%20%20let%20res%3B%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20res%20%3D%20await%20fetch(ENDPOINT%2C%20%7B%0A%20%20%20%20%20%20%20%20method%3A%20%22POST%22%2C%0A%20%20%20%20%20%20%20%20credentials%3A%20%22same-origin%22%2C%0A%20%20%20%20%20%20%20%20headers%3A%20%7B%20%22Content-Type%22%3A%20%22application%2Fx-www-form-urlencoded%3B%20charset%3DUTF-8%22%20%7D%2C%0A%20%20%20%20%20%20%20%20body%3A%20buildTemplate(input)%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20window.alert(%22Could%20not%20reach%20Rock.%20Check%20your%20connection%20and%20that%20you%20are%20still%20logged%20in.%22)%3B%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20(res.status%20!%3D%3D%20200)%20%7B%0A%20%20%20%20%20%20window.alert(messageForStatus(res.status%2C%20res.statusText))%3B%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20raw%20%3D%20await%20res.text()%3B%0A%20%20%20%20let%20data%3B%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20let%20parsed%20%3D%20JSON.parse(raw)%3B%0A%20%20%20%20%20%20if%20(typeof%20parsed%20%3D%3D%3D%20%22string%22)%20parsed%20%3D%20JSON.parse(parsed)%3B%20%2F%2F%20double-decode%0A%20%20%20%20%20%20data%20%3D%20parsed%3B%0A%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20window.alert(%22Received%20an%20unexpected%20response%20from%20Rock.%5Cn%5Cn%22%20%2B%20raw.slice(0%2C%20300))%3B%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if%20(data%20%26%26%20data.ErrCode%20%3D%3D%3D%200)%20%7B%0A%20%20%20%20%20%20const%20val%20%3D%20String(data.Val)%3B%0A%20%20%20%20%20%20const%20copied%20%3D%20await%20copyToClipboard(val)%3B%0A%20%20%20%20%20%20const%20tail%20%3D%20copied%0A%20%20%20%20%20%20%20%20%3F%20%22%5Cn%5Cn(Copied%20to%20your%20clipboard.)%22%0A%20%20%20%20%20%20%20%20%3A%20%22%5Cn%5Cn(Could%20not%20copy%20to%20the%20clipboard%3B%20copy%20it%20manually%20from%20above.)%22%3B%0A%20%20%20%20%20%20window.alert((data.Msg%20%7C%7C%20%22%22)%20%2B%20val%20%2B%20tail)%3B%0A%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20window.alert(data%20%26%26%20data.ErrMsg%20%3F%20data.ErrMsg%20%3A%20%22The%20value%20could%20not%20be%20translated.%22)%3B%0A%20%20%20%20%7D%0A%20%20%7D)()%3B%0A%7D%0A%0ArockKeyTranslate()%3B%0A%7D)()%3B
Maintainers only: the bookmarklet is generated from the core (
extension/core.js). See the bookmarkletreadmefile for details on regenerating this after making changes to the core.
- Make sure you are logged into your Rock instance.
- Select a Key (IdHash) or an Id on the page. Alternatively, trigger the tool with nothing selected and enter the value when prompted.
- Trigger the tool:
- Click the bookmarklet if you used that method.
- Or if you have the extension installed, you can click the extension toolbar button, use the right-click menu, or use the keyboard shortcut (
Alt+Shift+Kby default).
- The translated value is copied to your clipboard, and an alert shows the result and confirms whether the copy succeeded.
- Requires an HTTPS Rock page and a valid login session.
- The account you are signed in as must be permitted to use the
/api/Lava/RenderTemplateendpoint. - No API keys or secrets are stored; authentication uses your existing session cookie.
See REQUIREMENTS.md for the complete specification, including the response contract, error handling, and security considerations.
This repository contains:
- Browser extension for Chromium (Chrome and Edge) and Firefox. This is the primary, recommended way to use the tool. It adds a toolbar button, a right-click menu item on selected text, and a keyboard shortcut, all of which work even when a Rock modal dialog is open.
- Bookmarklet version for anyone who prefers not to install an extension (see below).
- A shared core so both forms behave identically.
REQUIREMENTS.md: the full scope and requirements specification.
extension/ the browser extension (primary deliverable)
core.js canonical shared core (the translation logic)
background.js service worker: wires the triggers to the core
manifest.json Chromium (Chrome / Edge)
manifest.firefox.json Firefox
icons/ toolbar / store icons (16/32/48/128 px PNG)
bookmarklet/ the bookmarklet form (generated from extension/core.js)
build.mjs generates the bookmarklet from the canonical core
The translation logic lives once, in extension/core.js. The bookmarklet is generated from it so the two forms never drift apart.