Break editor into a module
This commit is contained in:
@@ -1,144 +1,109 @@
|
||||
/**
|
||||
* Message catalog (S23).
|
||||
* Host message catalog.
|
||||
*
|
||||
* Flat key → string map. Intentionally not nested — the dot in a key is
|
||||
* just punctuation, the lookup is a plain object access. Flat keys are
|
||||
* easier to grep ("where do we use this string?"), easier to diff in
|
||||
* code review, and avoid the bikeshedding around when to nest vs flatten
|
||||
* a category. They also work transparently with the lightweight `t()`
|
||||
* helper in `./t.js` — no recursion, no path syntax to learn.
|
||||
* Scope
|
||||
* ─────
|
||||
* HOST-OWNED strings only — the chrome around the editor (header,
|
||||
* page title, PWA install banner, offline indicator, share-sheet
|
||||
* title). Strings inside the goods-editor module (sidebar tabs,
|
||||
* toolbar buttons, toasts, modal copy) have their OWN catalog
|
||||
* inside the module and are not re-exported. That separation is
|
||||
* deliberate: the host shouldn't be able to silently change the
|
||||
* module's user-facing copy, and the module shouldn't be able to
|
||||
* silently change the host's.
|
||||
*
|
||||
* Adding a key:
|
||||
* 1. Append it to the `en` map below with a key that matches the
|
||||
* strings in code (kebab namespace, e.g. `header.save-button`).
|
||||
* 2. Use it via `t('header.save-button')` in the component.
|
||||
* 3. When future locales are added, copy the en map to a new file
|
||||
* (es.js, fr.js…) and translate the values.
|
||||
* Convention — flat keys
|
||||
* ──────────────────────
|
||||
* The dot in `header.save` is punctuation, not nesting. The lookup
|
||||
* is a single object access. This makes:
|
||||
* • grepping ("where do we use this string?") trivial,
|
||||
* • diffs readable in code review,
|
||||
* • the t() helper a one-liner with no recursion or path syntax.
|
||||
*
|
||||
* Parameter syntax: `{name}` placeholders inside a value are replaced by
|
||||
* the matching key in the params object passed to t():
|
||||
* Adding a key
|
||||
* ────────────
|
||||
* 1. Append below with a key matching the namespace it belongs to
|
||||
* (header.*, pwa.*, offline.*, share.*).
|
||||
* 2. Use it via t('namespace.key') in the component.
|
||||
* 3. When new locales land, copy this file (es.js, fr.js…) and
|
||||
* translate the values. The t() helper picks the active catalog
|
||||
* via the import in t.js.
|
||||
*
|
||||
* 'cart.items-count': 'You have {count} items in your cart.'
|
||||
* t('cart.items-count', { count: 3 }) → 'You have 3 items in your cart.'
|
||||
* Placeholders
|
||||
* ────────────
|
||||
* `{name}` placeholders are substituted with the matching key in the
|
||||
* params object passed to t():
|
||||
*
|
||||
* If a key is missing, t() returns the key string itself — that's
|
||||
* obvious in the UI ("header.save-button" is not English) so missing
|
||||
* keys surface immediately in QA rather than silently rendering empty.
|
||||
* 'header.logo-home-aria': '{app} home'
|
||||
* t('header.logo-home-aria', { app: 'Pawfectly Yours' }) → 'Pawfectly Yours home'
|
||||
*
|
||||
* Pluralization
|
||||
* ─────────────
|
||||
* No ICU plurals — we use the explicit `.singular` / `.plural` key
|
||||
* convention, same pattern as the module's `bounds.singular` /
|
||||
* `bounds.plural`. Caller picks the key based on count:
|
||||
*
|
||||
* t(count === 1 ? 'header.cart-aria.singular' : 'header.cart-aria.plural',
|
||||
* { count })
|
||||
*
|
||||
* If we ever need real plural rules (Polish, Arabic, etc.) we'll move
|
||||
* to formatjs/intl-messageformat then. For en-only at this scale,
|
||||
* .singular/.plural is enough.
|
||||
*/
|
||||
|
||||
export const en = {
|
||||
// Header
|
||||
// ── Header ──────────────────────────────────────────────────
|
||||
// Save / Clear / Menu were removed from the header chrome during the
|
||||
// module split — Save was redundant with Download (same export
|
||||
// pipeline), Clear needs a module-side `clear()` method on the editor
|
||||
// ref before it can be reintroduced, and Menu opened the host's own
|
||||
// bottom sheet which is now inside the module. Their i18n keys are
|
||||
// gone with them; readd if you put any of those buttons back.
|
||||
'header.app-name': 'Pawfectly Yours',
|
||||
'header.app-tagline': 'Wear Your Pet. Share Your Story.',
|
||||
'header.page-title': 'Customize Your Shirt',
|
||||
'header.undo': 'Undo',
|
||||
'header.undo-tooltip': 'Undo (\u2318Z)',
|
||||
'header.redo': 'Redo',
|
||||
'header.redo-tooltip': 'Redo (\u2318\u21E7Z)',
|
||||
'header.clear': 'Clear canvas',
|
||||
'header.clear-tooltip': 'Clear all elements',
|
||||
'header.logo-home-aria': '{app} home',
|
||||
'header.preview': 'Print preview',
|
||||
'header.preview-tooltip': 'See your design at print resolution',
|
||||
'header.download': 'Download print file',
|
||||
'header.download-tooltip': 'Download the print-resolution PNG',
|
||||
'header.save': 'Save',
|
||||
'header.share': 'Share',
|
||||
'header.cart-aria': 'Cart, {count} {plural}',
|
||||
'header.menu': 'Menu',
|
||||
// Cart aria-label has two forms because pluralization affects the
|
||||
// whole phrase. The caller picks the key based on count.
|
||||
'header.cart-aria.singular': 'Cart, {count} item',
|
||||
'header.cart-aria.plural': 'Cart, {count} items',
|
||||
|
||||
// Toasts
|
||||
'toast.add-something-first': 'Add something to your design first.',
|
||||
'toast.cart-empty': 'Your cart is empty. Customize your shirt and add it to cart!',
|
||||
'toast.cart-items': 'You have {count} {plural} in your cart.',
|
||||
// ── Offline indicator ───────────────────────────────────────
|
||||
'offline.message': 'You\u2019re offline \u2014 changes are saved locally',
|
||||
|
||||
// ── PWA install / update prompts ────────────────────────────
|
||||
'pwa.install-message': 'Install Apparel Designer for offline access!',
|
||||
'pwa.install': 'Install',
|
||||
'pwa.dismiss': 'Later',
|
||||
'pwa.update-message': 'New version available!',
|
||||
'pwa.refresh': 'Refresh',
|
||||
'pwa.close': 'Close',
|
||||
|
||||
// ── Share sheet ─────────────────────────────────────────────
|
||||
// Title and body for the OS share sheet (iOS / Android / Web Share
|
||||
// API). `share.title` is also passed to <ApparelDesigner shareTitle=…/>
|
||||
// so the module uses the same string when invoking navigator.share()
|
||||
// from inside its own save / share flows.
|
||||
'share.title': 'My Pawfectly Yours design',
|
||||
'share.text': 'Check out the shirt I made!',
|
||||
|
||||
// ── Toasts ──────────────────────────────────────────────────
|
||||
// The host's toast surface (see src/components/Toast.jsx). The
|
||||
// editor module has its own toast catalog (`toast.crop-…` etc.)
|
||||
// inside the module — these are only the ones the HOST surfaces.
|
||||
// Kept narrowly scoped so we don't drift from the module's copy
|
||||
// for the same situation.
|
||||
'toast.dismiss': 'Dismiss',
|
||||
'toast.link-copied': 'Link copied to clipboard \u2728',
|
||||
// 'toast.copy-failed' interpolates the URL so the user can read
|
||||
// and copy it manually when both navigator.share and
|
||||
// navigator.clipboard are unavailable. Longer auto-dismiss (8s)
|
||||
// because the user needs time to read and copy.
|
||||
'toast.copy-failed': 'Couldn\u2019t auto-copy. URL: {url}',
|
||||
'toast.cleared': 'Canvas cleared. Use Undo to restore.',
|
||||
'toast.clear-confirm': 'Click Clear again to confirm.',
|
||||
'toast.unknown-template': 'Unknown template: {id}',
|
||||
'toast.image-load-failed': 'Failed to load image. Try a different file.',
|
||||
'toast.save-failed': 'Save failed: {error}',
|
||||
'toast.saved': 'Saved!',
|
||||
// Crop refuses on rotated elements because the overlay (dim mask,
|
||||
// crop rect, transformer anchors) is axis-aligned and wouldn't
|
||||
// visually match a rotated image. Surfaced as an info toast rather
|
||||
// than silently hiding the Crop button — the user gets a clear
|
||||
// explanation of why nothing happened and what to try instead.
|
||||
// 4-second duration in App.jsx so the user has time to read the
|
||||
// suggested action.
|
||||
'toast.crop-rotation-blocked': 'Reset rotation to 0° before cropping. Press ⌘Z to undo any rotation first.',
|
||||
|
||||
// Apply Crop fired before the image had finished loading its pixel
|
||||
// data into the Konva node — we can't compute the source crop
|
||||
// region without natural dimensions, so the apply is a no-op and
|
||||
// we surface this so the user knows to try again in a moment.
|
||||
// App.jsx's handleApplyCrop also keeps crop mode active in this
|
||||
// case (rather than silently closing it), so the user can simply
|
||||
// click Apply again once the image has decoded.
|
||||
'toast.crop-not-ready': 'Photo is still loading. Try Apply again in a moment.',
|
||||
|
||||
// Cart
|
||||
'cart.disabled-out-of-bounds': 'Move everything inside the print area to add to cart.',
|
||||
'cart.add': 'Add to Cart',
|
||||
|
||||
// Sidebar tabs
|
||||
'sidebar.tab.upload': 'Upload Photo',
|
||||
'sidebar.tab.stickers': 'Stickers',
|
||||
'sidebar.tab.emoji': 'Emoji',
|
||||
'sidebar.tab.text': 'Text',
|
||||
'sidebar.tab.layers': 'Layers',
|
||||
'sidebar.preview-on-model': 'Preview on Model',
|
||||
|
||||
// Text tab
|
||||
'text.field.message': 'Your message',
|
||||
'text.placeholder': 'e.g. Best Pup Ever',
|
||||
'text.field.font': 'Font',
|
||||
'text.field.size': 'Size',
|
||||
'text.field.color': 'Color',
|
||||
'text.field.outline': 'Outline',
|
||||
'text.field.arc': 'Arc',
|
||||
'text.editing-banner': 'Editing selected text',
|
||||
'text.recent-colors': 'Recent',
|
||||
'text.contrast-warning': 'Low contrast with shirt color \u2014 your text may be hard to see.',
|
||||
'text.arc-flat': 'Flat',
|
||||
'text.arc-slight-up': 'Slight curve up',
|
||||
'text.arc-slight-down': 'Slight curve down',
|
||||
'text.arc-medium-up': 'Medium curve up',
|
||||
'text.arc-medium-down': 'Medium curve down',
|
||||
'text.arc-strong-up': 'Strong curve up',
|
||||
'text.arc-strong-down': 'Strong curve down',
|
||||
'text.arc-reset': 'Reset to flat',
|
||||
'text.preview-fallback': 'Preview',
|
||||
'text.add-button': 'Add text to canvas',
|
||||
|
||||
// Element toolbar
|
||||
'toolbar.delete': 'Delete',
|
||||
'toolbar.duplicate': 'Duplicate',
|
||||
'toolbar.bring-forward': 'Bring forward',
|
||||
'toolbar.send-backward': 'Send backward',
|
||||
'toolbar.flip-h': 'Flip horizontally',
|
||||
'toolbar.flip-v': 'Flip vertically',
|
||||
'toolbar.lock': 'Lock',
|
||||
'toolbar.unlock': 'Unlock',
|
||||
'toolbar.opacity': 'Opacity',
|
||||
'toolbar.size': 'Size',
|
||||
'toolbar.edit-photo': 'Edit Photo',
|
||||
|
||||
// Layers
|
||||
'layers.title': 'Layers ({count})',
|
||||
'layers.empty': 'No elements yet. Add images, text, or stickers to your design.',
|
||||
'layers.delete-many': 'Delete {count} selected',
|
||||
'layers.dragHandle-tooltip': 'Drag to reorder',
|
||||
|
||||
// Bounds warning
|
||||
'bounds.singular': '1 element is outside the print area and won\u2019t be printed.',
|
||||
'bounds.plural': '{count} elements are outside the print area and won\u2019t be printed.',
|
||||
|
||||
// Preview modal
|
||||
'preview.title': 'Print preview',
|
||||
'preview.close': 'Close',
|
||||
'preview.loading': 'Rendering at print resolution\u2026',
|
||||
'preview.error': 'Couldn\u2019t render preview: {error}',
|
||||
'preview.hint': 'This is what will print on your shirt \u2014 4500\u00d74500 at 300 DPI.',
|
||||
'preview.keep-editing': 'Keep editing',
|
||||
'preview.download-png': 'Download PNG',
|
||||
'toast.upload-failed': 'Upload failed: {error}',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user