[object Object]

← back to Designer Wallcoverings

Fix accented-title casing (Unicode \p{L}) + re-title 46 Schumacher products

ac43bae87e7463264e55bf648d773c0fd1351803 · 2026-06-11 13:09:24 -0700 · SteveStudio2

capRuns used /[A-Za-z]+/ so Swedish names (KORALLÄNG) split at accents → 'KorallÄNg'.
Switched to /\p{L}+/gu. schu-fix-titles.js re-titles the 46 already-created products with
accented pattern/color names (+ strips doubled 'Wallcoverings' from color echoes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit ac43bae87e7463264e55bf648d773c0fd1351803
Author: SteveStudio2 <stevestudio2@SteveStudio2s-Mac-Studio.local>
Date:   Thu Jun 11 13:09:24 2026 -0700

    Fix accented-title casing (Unicode \p{L}) + re-title 46 Schumacher products
    
    capRuns used /[A-Za-z]+/ so Swedish names (KORALLÄNG) split at accents → 'KorallÄNg'.
    Switched to /\p{L}+/gu. schu-fix-titles.js re-titles the 46 already-created products with
    accented pattern/color names (+ strips doubled 'Wallcoverings' from color echoes).
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 shopify/scripts/cadence/cadence-import.js |  2 +-
 shopify/scripts/schu-fix-titles.js        | 56 +++++++++++++++++++++++++++++++
 2 files changed, 57 insertions(+), 1 deletion(-)

diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
index 7151a24d..7ad4e029 100644
--- a/shopify/scripts/cadence/cadence-import.js
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -56,7 +56,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
 const SMALL = new Set(['a','an','and','as','at','but','by','de','del','for','if','in','la','le','nor','of','on','or','per','the','to','und','via','vs','with']);
 const KEEP_UPPER = new Set(['usa','uk','ny','nyc','sf','clj','led','pvc','uv','3d','dna']);
 const decode = s => String(s||'').replace(/&amp;/gi,'&').replace(/&quot;/gi,'"').replace(/&#39;|&apos;/gi,"'").replace(/&nbsp;/gi,' ');
-const capRuns = w => w.replace(/[A-Za-z]+/g, (m, off, str) => {
+const capRuns = w => w.replace(/\p{L}+/gu, (m, off, str) => {
   const prev = str[off - 1];
   if ((prev === "'" || prev === '’') && m.length <= 2) return m.toLowerCase(); // possessive 's / contractions
   return m[0].toUpperCase() + m.slice(1).toLowerCase();
diff --git a/shopify/scripts/schu-fix-titles.js b/shopify/scripts/schu-fix-titles.js
new file mode 100644
index 00000000..a0d540e3
--- /dev/null
+++ b/shopify/scripts/schu-fix-titles.js
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+/**
+ * Fix mangled accented titles on already-created Schumacher DRAFT products.
+ *
+ * cadence-import's old capRuns used /[A-Za-z]+/ (ASCII), so Swedish names like KORALLÄNG
+ * title-cased to "KorallÄNg" (split at Ä). The importer is now fixed (\p{L}); this re-titles
+ * the products already on Shopify whose pattern/color contain accented chars.
+ *   node schu-fix-titles.js          # dry
+ *   node schu-fix-titles.js --commit
+ */
+const https = require('https'); const fs = require('fs'); const os = require('os');
+const { execFileSync } = require('child_process');
+const COMMIT = process.argv.includes('--commit');
+const TOKEN = (fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
+const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function psql(s) { return execFileSync('psql', ['-At', '-F', '\t', '-d', 'dw_unified', '-c', s], { encoding: 'utf8', maxBuffer: 1 << 28 }).trim(); }
+
+// --- title casing (Unicode-correct) ---
+const capRuns = w => w.replace(/\p{L}+/gu, (m, off, str) => { const p = str[off - 1]; if ((p === "'" || p === '’') && m.length <= 2) return m.toLowerCase(); return m[0].toUpperCase() + m.slice(1).toLowerCase(); });
+const stripCodes = s => String(s || '').replace(/\(\s*[A-Za-z]?\d{3,}[^)]*\)/g, '').replace(/\s{2,}/g, ' ').trim();
+function titleCase(s) { return String(s || '').trim().split(/\s+/).map(capRuns).join(' '); }
+
+function gql(query, variables) {
+  return new Promise(res => { const data = JSON.stringify({ query, variables });
+    const req = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
+      r => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { res(JSON.parse(d)); } catch { res({}); } }); });
+    req.on('error', () => res({})); req.write(data); req.end(); });
+}
+async function gqlRetry(q, v) { for (let a = 0; a < 5; a++) { const r = await gql(q, v); if (r.errors && JSON.stringify(r.errors).includes('THROTTLED')) { await sleep(2000 * (a + 1)); continue; } await sleep(350); return r; } return {}; }
+
+(async () => {
+  if (!TOKEN) { console.error('no token'); process.exit(1); }
+  const rows = psql(`SELECT dw_sku, shopify_product_id, pattern_name, color_name FROM schumacher_catalog
+    WHERE shopify_product_id IS NOT NULL AND shopify_product_id<>''
+    AND (pattern_name ~ '[À-ſ]' OR color_name ~ '[À-ſ]');`).split('\n').filter(Boolean)
+    .map(l => { const [dw, pid, pat, col] = l.split('\t'); return { dw, pid, pat, col }; });
+  console.log(`affected products: ${rows.length}`);
+  const mUpd = `mutation($input:ProductInput!){productUpdate(input:$input){product{id title} userErrors{field message}}}`;
+  let ok = 0, fail = 0, n = 0;
+  for (const r of rows) {
+    const pattern = titleCase(stripCodes(r.pat || ''));
+    const color = r.col ? titleCase(stripCodes(r.col)) : '';
+    let core = color ? `${pattern}, ${color}` : pattern;
+    core = core.replace(/[\s,]*wallcoverings?\s*$/i, '').replace(/[\s,]*wallpapers?\s*$/i, '').trim(); // avoid double "Wallcoverings"
+    const title = `${core} Wallcoverings | Schumacher`;
+    if (n < 12) { console.log(`  ${r.dw}  "${r.pat}${r.col ? ', ' + r.col : ''}" → "${title}"`); n++; }
+    if (!COMMIT) continue;
+    const gid = `gid://shopify/Product/${r.pid}`;
+    const res = await gqlRetry(mUpd, { input: { id: gid, title } });
+    const ue = res?.data?.productUpdate?.userErrors;
+    if (ue && ue.length) { fail++; console.log(`  ✗ ${r.dw} ${JSON.stringify(ue).slice(0, 100)}`); }
+    else { ok++; if (ok % 10 === 0) process.stdout.write(`\r  fixed ${ok}/${rows.length}…`); }
+  }
+  console.log(COMMIT ? `\nDONE: re-titled ${ok}, failed ${fail}.` : `\nDRY — re-run with --commit to apply.`);
+})();

← 7cc6bdfc Add fix-broken-rolls.js — guardrailed live-price fixer (dry-  ·  back to Designer Wallcoverings  ·  Add schu-activate: gated DRAFT→ACTIVE for Schumacher (price+ a49bb8e4 →