MobTranslate styleguide

Offline and Local Recovery

Preserve owned drafts and explicitly cached public data while distinguishing connection hints, stored snapshots and real synchronization.

Define what survives and where

State the exact local data, origin, history, freshness and removal policy. This example stores an owned English draft in versioned localStorage history and manually caches one public JSON fixture through CacheStorage. It does not install a service worker, cache the page shell or synchronize a server.

A connection hint is not request evidence. A saved client timestamp is not source freshness or permission. Private/cultural data requires a separately governed caching and expiration policy; these bytes are explicitly public demonstration data.

Anatomy and visible storage state

PartActual contract
Draft editorNative controlled Textarea independent from last observed saved text/revision; edits remain visible through failures and pending locks.
Save / restore44px minimum explicit commands. Save snapshots current text; restore preserves displaced editor text as a recoverable previous-editor copy.
Stored historyVersion=1 envelope with sequential revisions/text/client timestamps; successful writes append, then read back actual stored data.
Cross-tab boundaryNamed Web Lock serializes cooperating saves, plus a revision check rejects stale replacement. Storage events warn without overwriting the active editor.
Public resourceExplicit network load, byte-cache/readback and cached read with loaded titles and client cache timestamp.
Connection / recoverynavigator.onLine is only a hint. Actual fetch/cache/storage results drive success/failure; cancel preserves prior useful data.

Working stored draft and public cache

Save and reload the draft, test another tab’s changed revision, restore a stored snapshot and recover the displaced editor copy. Load/cache the actual public fixture and read its saved bytes when a network request cannot succeed.

Browser storage for an owned English-only draft and an explicitly public fixture. This does not install a service worker, cache the whole site, authorize private access or queue a server write.

Browser connection hint: online. Actual request outcomes, not this hint, decide whether a load succeeded.

Last observed stored revision: 0. Editor has unsaved changes.

No saved draft inspected yet.

No public resource loaded.

    Snapshots remain in this browser origin until browser policy/user action removes them. Versioned history preserves prior stored drafts; cross-tab locks serialize cooperating saves and stale revisions reject replacement. Browser eviction, disabled storage, quota failure and real authenticated synchronization require their own handling. No background synchronization or durable backend completion is claimed.

    tsx
    'use client';
    import {useEffect,useRef,useState,type CSSProperties} from 'react';
    import {Button,Textarea} from '@mobtranslate/ui';
    const draftKey='mt-styleguide-offline-draft-v1',cacheName='mt-styleguide-public-fixture-v1',resource='/fixtures/governed-records.json';
    type Entry={revision:number;text:string;savedAt:number};
    type Envelope={version:1;entries:Entry[]};
    const action:CSSProperties={minHeight:44,height:'auto',minWidth:0,maxWidth:'100%',whiteSpace:'normal',overflowWrap:'anywhere'};
    function stored():Envelope{
      const raw=localStorage.getItem(draftKey);if(raw===null)return {version:1,entries:[]};
      const value:unknown=JSON.parse(raw);if(!value||typeof value!=='object'||!('version'in value)||value.version!==1||!('entries'in value)||!Array.isArray(value.entries)||!value.entries.every((entry,index)=>entry&&typeof entry==='object'&&entry.revision===index+1&&typeof entry.text==='string'&&Number.isFinite(entry.savedAt)))throw Error('Stored draft format is invalid; existing bytes were preserved.');
      return value as Envelope;
    }
    export function OfflineDraftExample(){
      const [draft,setDraft]=useState('English-only local offline draft.'),[savedText,setSavedText]=useState<string|null>(null),[revision,setRevision]=useState(0),[backup,setBackup]=useState<string|null>(null),[busy,setBusy]=useState(false),[online,setOnline]=useState(true),[message,setMessage]=useState('No saved draft inspected yet.'),[resourceMessage,setResourceMessage]=useState('No public resource loaded.'),[titles,setTitles]=useState<string[]>([]),[rtl,setRtl]=useState(false);
      const base=useRef(0),live=useRef(false),saveAbort=useRef<AbortController|null>(null),abort=useRef<AbortController|null>(null),epoch=useRef(0);
      useEffect(()=>{live.current=true;function connection(){setOnline(navigator.onLine);}connection();try{const last=stored().entries.at(-1);if(last){base.current=last.revision;setRevision(last.revision);setDraft(last.text);setSavedText(last.text);setMessage(`Restored stored draft revision ${last.revision}.`);}else setMessage('No stored draft yet; current editor is local only.');}catch(error){setMessage(error instanceof Error?error.message:'Browser storage is unavailable.');}const changed=(event:StorageEvent)=>{if(event.key===draftKey)setMessage('Another tab changed stored draft data. Your editor remains; restore a snapshot before attempting a conflicting save.');};window.addEventListener('online',connection);window.addEventListener('offline',connection);window.addEventListener('storage',changed);return()=>{live.current=false;epoch.current++;abort.current?.abort();saveAbort.current?.abort();window.removeEventListener('online',connection);window.removeEventListener('offline',connection);window.removeEventListener('storage',changed);};},[]);
      async function save(){if(busy)return;const text=draft;const saving=new AbortController();saveAbort.current=saving;setBusy(true);setMessage('Waiting for the browser save lock; the current editor remains editable.');try{if(!navigator.locks)throw Error('Cross-tab locks unavailable; draft retained without saving.');await navigator.locks.request(draftKey,{signal:saving.signal},()=>{if(!live.current||saving.signal.aborted)return;const previous=stored();const last=previous.entries.at(-1)?.revision??0;if(last!==base.current)throw Error('Stored draft changed in another tab. Local editor retained; restore the latest snapshot to reconcile.');const entry={revision:last+1,text,savedAt:Date.now()};const next:Envelope={version:1,entries:[...previous.entries,entry]};localStorage.setItem(draftKey,JSON.stringify(next));const readback=stored().entries.at(-1);if(readback?.revision!==entry.revision||readback.text!==entry.text)throw Error('Stored draft readback did not match; editor retained.');if(live.current){base.current=entry.revision;setRevision(entry.revision);setSavedText(text);setMessage(`Saved and read back browser draft revision ${entry.revision}; ${next.entries.length} ${next.entries.length===1?'snapshot':'snapshots'} retained.`);}});}catch(error){if(live.current)setMessage(error instanceof DOMException&&error.name==='AbortError'?'Waiting browser save cancelled; editor retained.':error instanceof Error?error.message:'Browser save failed; editor retained.');}finally{if(live.current)setBusy(false);}}
      function restore(){try{const last=stored().entries.at(-1);if(!last){setMessage('No stored snapshot to restore; editor retained.');return;}if(draft!==last.text)setBackup(draft);base.current=last.revision;setRevision(last.revision);setSavedText(last.text);setDraft(last.text);setMessage(`Restored revision ${last.revision}. Any displaced editor text is retained in the previous-editor copy below.`);}catch(error){setMessage(error instanceof Error?error.message:'Browser restore failed; editor retained.');}}
      async function load(from:'network'|'cache'){
        abort.current?.abort();const controller=new AbortController();abort.current=controller;const identity=++epoch.current;setResourceMessage(from==='network'?'Loading public fixture over HTTP; prior loaded content remains.':'Reading explicitly cached public fixture; prior loaded content remains.');
        try{const response=from==='network'?await fetch(resource,{signal:controller.signal,cache:'no-store'}):await (await caches.open(cacheName)).match(resource);if(!response)throw Error('No cached public fixture. Load and cache it while a request can succeed.');if(!response.ok)throw Error(`Public fixture returned HTTP ${response.status}.`);const data:unknown=await response.json();if(!Array.isArray(data)||!data.every(row=>row&&typeof row.title==='string'))throw Error('Public fixture shape is invalid.');if(controller.signal.aborted||identity!==epoch.current||!live.current)return;setTitles(data.map(row=>row.title));const stamp=response.headers.get('x-styleguide-saved-at');setResourceMessage(`Loaded ${data.length} public fixture titles from ${from}. ${from==='cache'?`Cached client timestamp: ${stamp??'unknown'}. This is a snapshot, not current source authority.`:'No private access or language review is inferred.'}`);}catch(error){if(controller.signal.aborted||identity!==epoch.current||!live.current)return;setResourceMessage(`${error instanceof Error?error.message:'Resource load failed.'} Previous loaded titles and draft remain.`);}
      }
      async function cache(){abort.current?.abort();const controller=new AbortController();abort.current=controller;const identity=++epoch.current;setResourceMessage('Fetching public bytes for the explicit cache operation; prior data remains.');try{const response=await fetch(resource,{cache:'no-store',signal:controller.signal});if(!response.ok)throw Error(`Cache request returned HTTP ${response.status}.`);const bytes=await response.arrayBuffer();if(controller.signal.aborted||identity!==epoch.current||!live.current)return;const saved=new Response(bytes,{status:200,headers:{'content-type':'application/json','x-styleguide-saved-at':new Date().toISOString()}});const store=await caches.open(cacheName);await store.put(resource,saved);const readback=await store.match(resource);if(!readback||!(await readback.arrayBuffer()).byteLength)throw Error('Cached resource readback missing.');if(live.current&&identity===epoch.current&&!controller.signal.aborted)setResourceMessage('Public fixture cached and read back. This caches its bytes only; no page shell or confidential record was saved.');}catch(error){if(live.current&&identity===epoch.current&&!controller.signal.aborted)setResourceMessage(`${error instanceof Error?error.message:'Cache unavailable.'} Existing draft and loaded titles remain.`);}}
      return <div data-offline-lab lang="en" dir={rtl?'rtl':'ltr'} className="space-y-4" style={{overflowWrap:'anywhere'}}>
        <p dir="auto">Browser storage for an owned English-only draft and an explicitly public fixture. This does not install a service worker, cache the whole site, authorize private access or queue a server write.</p>
        <p dir="auto" data-offline-connection>Browser connection hint: {online?'online':'offline'}. Actual request outcomes, not this hint, decide whether a load succeeded.</p>
        <label htmlFor="offline-owned-draft" className="block">Owned local browser draft</label><Textarea id="offline-owned-draft" dir="auto" value={draft} onChange={event=>setDraft(event.target.value)} />
        <p dir="auto" data-offline-revision>Last observed stored revision: {revision}. Editor {draft===savedText?'matches that saved snapshot':'has unsaved changes'}.</p>
        <div className="flex flex-wrap gap-3"><Button style={action} type="button" disabled={busy} onClick={()=>void save()}>Save browser draft snapshot</Button><Button style={action} type="button" variant="outline" disabled={busy} onClick={restore}>Restore latest browser snapshot</Button>{backup!==null&&<Button style={action} type="button" variant="outline" onClick={()=>{const current=draft;setDraft(backup);setBackup(current);setMessage('Previous editor copy restored; stored history remains unchanged.');}}>Restore previous editor copy</Button>}</div>
        {busy&&<Button style={action} type="button" variant="outline" onClick={()=>saveAbort.current?.abort()}>Cancel waiting browser save</Button>}<p role="status" dir="auto" data-offline-draft-status>{message}</p>
        <div className="flex flex-wrap gap-3"><Button style={action} type="button" variant="outline" onClick={()=>void load('network')}>Load public fixture over network</Button><Button style={action} type="button" variant="outline" onClick={()=>void cache()}>Cache public fixture bytes</Button><Button style={action} type="button" variant="outline" onClick={()=>void load('cache')}>Read cached public fixture</Button><Button style={action} type="button" variant="outline" onClick={()=>{epoch.current++;abort.current?.abort();setResourceMessage('Public resource request cancelled; prior content and draft remain. A cache write already started may finish; cancellation does not delete saved bytes.');}}>Cancel public resource load</Button><Button style={action} type="button" variant="outline" aria-pressed={rtl} onClick={()=>setRtl(value=>!value)}>Use RTL offline layout</Button></div>
        <p role="status" dir="auto" data-offline-resource-status>{resourceMessage}</p><ul className="guidance-list" data-offline-titles>{titles.map(title=><li dir="auto" key={title}>{title}</li>)}</ul>
        <p dir="auto">Snapshots remain in this browser origin until browser policy/user action removes them. Versioned history preserves prior stored drafts; cross-tab locks serialize cooperating saves and stale revisions reject replacement. Browser eviction, disabled storage, quota failure and real authenticated synchronization require their own handling. No background synchronization or durable backend completion is claimed.</p>
      </div>;
    }
    

    Save, conflict and history lifetime

    • Save captures text before waiting for a cooperating lock. The editor may change while waiting; readback only claims the captured snapshot, and the current editor’s dirty state remains visible.
    • Inside the lock, validate the stored envelope and compare the latest revision with the last observed base. A conflict preserves both stored history and local editor text instead of silently overwriting another tab.
    • Restoring latest explicitly replaces the editor but retains its displaced copy. Restore Previous Editor Copy changes the editor again without deleting stored snapshots. A subsequent save appends a new revision.
    • Abort can cancel a waiting lock request; the synchronous storage commit is not retroactively undone. Completed stored versions remain in history. Unmount aborts queued work and suppresses stale UI updates.
    • Web Locks coordinate same-origin cooperating clients, not malicious scripts or an authenticated backend transaction. Missing locks/storage, invalid stored shape and quota failure keep the draft without a false saved claim.

    Network versus cached resource lifetime

    • A successful network load displays actual public fixture titles. Explicit caching stores the fetched bytes with a client timestamp and verifies a nonempty cache readback.
    • Read Cached uses CacheStorage and can work without a successful network request in the already-open page. This does not prove offline page navigation/reload because no shell/service worker is cached.
    • A new load/cache operation supersedes the earlier request and checks identity before applying UI state. Cancel aborts transport and retains previous loaded titles/draft.
    • A Cache.put already started can finish despite cancellation. State that boundary honestly; cancellation is not deletion or a guarantee that saved bytes disappeared.
    • Cache availability requires the relevant browser/security/storage context. A missing/evicted/denied cache produces actual recovery text, not fabricated records.

    State matrix and recovery choices

    StateWhat to retain and explain
    Unsaved / pending saveCurrent editor plus captured snapshot distinction; explicit waiting cancellation.
    Stored / restoredActual readback version/history and the storage origin/scope; no backend-completion claim.
    ConflictBoth local editor and newer stored history; explicit restore/reconciliation before another append.
    Storage denied / quota / corruptKeep editor and existing bytes, report failure and offer a permitted alternative. Never overwrite an unknown format as empty.
    Network failed / cached snapshotLast useful loaded data/draft plus actual source/freshness boundary.
    Expired permission / private dataApply real access/retention policy before reuse; a local cache is not an authorization grant.

    Persistence, privacy and synchronization boundaries

    • Browser storage can be denied, cleared or evicted. Readback is evidence of the stored value at that time, not a universal durability guarantee.
    • Do not cache confidential or culturally restricted data just because a request once succeeded. Declare permitted audience, expiry, revocation and offline-use policy with actual source/authorization owners.
    • A real synchronization queue needs stable IDs, version/conflict policy, duplicate protection, partial failures and verified remote acknowledgements. No such queue or service is fabricated here.
    • Never call a local snapshot uploaded, published, reviewed or remotely saved. Preserve the exact difference between editor, storage, cache and persisted service outcomes.

    API, accessibility and adaptation

    OwnerNative/exported responsibility
    Storage / coordinationlocalStorage schema/readback plus navigator.locks.request with AbortSignal; native storage event invalidation.
    Resource cacheSame-origin fetch/AbortController, named CacheStorage Cache and response readback. No private authentication or service-worker export.
    UIExported Textarea/Button plus visible status/revision/dirty text. No new OfflineStore package API or silent auto-save.
    Keyboard / focusOrdinary native controls; preserve draft focus through unrelated resource arrivals. Long data/error/status strings wrap at 320px and in RTL.

    Acceptance and scope

    • Run test:learning-offline, audio:check, source/token freshness, lint/types/build and the complete route/fragment crawl. Inspect actual changed values, result/state ownership, recovery, keyboard focus and narrow theme/direction geometry.
    • Browser model, storage readback and media-clock evidence do not establish language fluency, qualified review, confidential authorization, physical speaker output or durable backend synchronization. Verify those consuming systems separately.