chore: redesign article localization before implementation
This commit is contained in:
1 parent
99a74dc3a2
commit
8c4a1825ab
31 files changed
+3365
-882
No files matched your search
@@ -0,0 +1,101 @@
|
||||
// Shared localization fixtures. One source for the Localization board (LocaleFrame), the Prototype (LocaleLive) and the Handoff.
|
||||
// Dataset "Journal": 8 stories, 17 versions. Every count shown anywhere is derived from STORIES or LANGS.
|
||||
// Separate fixtures (marked): PRESS (all stories translated, for the nothing-missing state), JOURNAL_PRE_FR (prototype start: French not enabled yet).
|
||||
(function () {
|
||||
const P = {
|
||||
en: ['Most quotes fail because they hide the same three costs. Here is how we itemise them before the first tile is ordered.', 'Start with the room, not the catalogue. Measure the walls, note where the pipes run and photograph the existing fixtures.', 'Next, separate what is fixed from what is chosen. Plumbing and waterproofing are fixed. Tiles and taps are chosen.', 'Price the fixed items first and ask for a written range. A range with a stated assumption is more useful than a single figure.', 'Allow ten percent for what the walls reveal. Old bathrooms almost always hide one surprise behind the tiles.', 'Check the floor falls toward the drain before any membrane goes down. Correcting a fall after tiling means lifting everything.', 'Choose the tiles after the layout is drawn. A tile that needs many cuts costs more in labour than it saves in material.', 'Ask the tiler to confirm the adhesive and grout for the wet area in writing, and keep the data sheets with the quote.', 'Agree who protects the hallway floor and who removes the old bath. These small jobs are the ones that start disputes.', 'Finally, agree the order of work with the tiler and the plumber before anything is ordered.'],
|
||||
fr: ['La plupart des devis échouent parce qu\'ils cachent les trois mêmes coûts. Voici comment nous les détaillons avant de commander le premier carreau.', 'Commencez par la pièce, pas par le catalogue. Mesurez les murs, repérez le passage des canalisations et photographiez les équipements existants.', 'Séparez ensuite ce qui est fixe de ce qui est choisi. La plomberie et l\'étanchéité sont fixes. Les carreaux et la robinetterie sont choisis.', 'Chiffrez d\'abord les postes fixes et demandez une fourchette écrite.'],
|
||||
de: ['Die meisten Angebote scheitern, weil sie dieselben drei Kosten verschweigen. So listen wir sie auf, bevor die erste Fliese bestellt wird.', 'Beginnen Sie mit dem Raum, nicht mit dem Katalog. Messen Sie die Wände und halten Sie fest, wo die Rohre verlaufen.'],
|
||||
ar: ['تفشل معظم عروض الأسعار لأنها تُخفي التكاليف الثلاثة نفسها. إليك كيف نفصّلها قبل طلب أول بلاطة.', 'ابدأ بالغرفة لا بالكتالوج. قِس الجدران، وحدّد مسار الأنابيب، وصوّر التركيبات الحالية.', 'افصل بعد ذلك ما هو ثابت عمّا هو مختار. السباكة والعزل ثابتان، أما البلاط والصنابير فمختارة.']
|
||||
}
|
||||
const LANG_NAMES = { en: 'English', fr: 'French', de: 'German', ar: 'Arabic', es: 'Spanish', it: 'Italian', 'pt-BR': 'Portuguese (Brazil)', 'en-GB': 'English (United Kingdom)' }
|
||||
const DIR = { ar: 'rtl' }
|
||||
// v: lang -> [status, needsReview, revision]
|
||||
const mk = (id, title, v, tr, by, upd) => ({ id, title, v, tr: tr || {}, by, upd })
|
||||
const STORIES = [
|
||||
mk('s1', 'How we price a bathroom renovation', { en: ['pub', 0, 8], fr: ['draft', 1, 5], de: ['pub', 0, 3], ar: ['draft', 1, 2] }, { fr: 'Comment nous chiffrons une rénovation de salle de bain', de: 'So kalkulieren wir eine Badsanierung', ar: 'كيف نسعّر تجديد الحمّام' }, 'Marta Alves', '2 h ago'),
|
||||
mk('s2', 'A guide to lime plaster', { en: ['pub', 0, 4], fr: ['pub', 0, 4], de: ['pub', 0, 2] }, { fr: 'Guide de l\'enduit à la chaux', de: 'Ein Leitfaden zu Kalkputz' }, 'Camille Roche', 'Yesterday'),
|
||||
mk('s3', 'Winter checklist for timber windows', { en: ['pub', 0, 6], fr: ['pub', 1, 3] }, { fr: 'Liste d\'hiver pour les fenêtres en bois' }, 'Luis Ferrer', '3 days ago'),
|
||||
mk('s4', 'Spring newsletter', { en: ['draft', 0, 1] }, {}, 'Marta Alves', '3 days ago'),
|
||||
mk('s5', 'Roof inspection checklist', { en: ['pub', 0, 3], de: ['draft', 0, 1] }, { de: 'Checkliste für die Dachprüfung' }, 'Luis Ferrer', '14 Oct'),
|
||||
mk('s6', 'Why we reuse original floorboards', { en: ['pub', 0, 2], fr: ['draft', 0, 1] }, { fr: 'Pourquoi nous réutilisons les lames de parquet d\'origine' }, 'Camille Roche', '13 Oct'),
|
||||
mk('s7', 'Our first passive-house retrofit', { en: ['pub', 0, 5], de: ['pub', 0, 2] }, { de: 'Unsere erste Passivhaus-Sanierung' }, 'Marta Alves', '12 Oct'),
|
||||
mk('s8', 'Cómo elegir un contratista', { es: ['pub', 0, 2] }, {}, 'Luis Ferrer', '10 Oct')
|
||||
]
|
||||
const ENABLED = ['en', 'fr', 'de', 'ar']
|
||||
const LANGS = [
|
||||
{ name: 'English', code: 'en', def: true, enabled: true, social: 'Not set' },
|
||||
{ name: 'French', code: 'fr', enabled: true, social: 'fr_FR' },
|
||||
{ name: 'German', code: 'de', enabled: true, social: 'Not set' },
|
||||
{ name: 'Arabic', code: 'ar', enabled: true, social: 'Not set' },
|
||||
{ name: 'Spanish', code: 'es', enabled: false, social: 'Not set' },
|
||||
{ name: 'Italian', code: 'it', enabled: false, social: 'Not set' }
|
||||
]
|
||||
const count = (code, stories) => { let n = 0, pub = 0; stories.forEach(s => { const x = s.v[code]; if (x) { n++; if (x[0] === 'pub') pub++ } }); return [n, pub] }
|
||||
const PRESS = [
|
||||
mk('p1', 'Opening hours over the holidays', { en: ['pub', 0, 2], fr: ['pub', 0, 2] }, { fr: 'Horaires pendant les fêtes' }, 'Marta Alves', 'Yesterday'),
|
||||
mk('p2', 'Meet the new site manager', { en: ['pub', 0, 3], fr: ['draft', 1, 1] }, { fr: 'Rencontrez la nouvelle cheffe de chantier' }, 'Luis Ferrer', '3 days ago'),
|
||||
mk('p3', 'Annual report', { en: ['draft', 0, 1], fr: ['pub', 0, 1] }, { fr: 'Rapport annuel' }, 'Marta Alves', '14 Oct')
|
||||
]
|
||||
// Prototype start: French is not enabled, so no French versions exist yet.
|
||||
const preFr = () => STORIES.map(s => { const v = { ...s.v }; delete v.fr; const tr = { ...s.tr }; delete tr.fr; return { ...s, v, tr } })
|
||||
// Setup fixture (before languages exist): articles per site, site locale as stored, the language entry it maps to.
|
||||
const SETUP = [
|
||||
{ site: 'Journal', n: 8, locale: 'en', tag: 'en', name: 'English', note: 'Site locale en' },
|
||||
{ site: 'Notes', n: 3, locale: null, tag: null, name: null, note: 'No site locale' },
|
||||
{ site: 'Archive', n: 5, locale: 'pt-BR', tag: 'pt-BR', name: 'Portuguese (Brazil)', note: 'Site locale pt-BR' }
|
||||
]
|
||||
|
||||
STORIES[3].seo = 1; STORIES[4].seo = 1
|
||||
Object.assign(LANG_NAMES, { 'zh-Hans': 'Chinese (Simplified)', ja: 'Japanese' })
|
||||
const ATELIER = [
|
||||
mk('a1', 'A complete guide to renovating a ground-floor kitchen and bathroom in a listed building, with every cost itemised for owners and tenants', { en: ['pub', 0, 4], 'en-GB': ['draft', 0, 1], fr: ['draft', 1, 2], de: ['pub', 0, 2], 'pt-BR': ['draft', 0, 1], 'zh-Hans': ['pub', 0, 2], ja: ['pub', 0, 2], ar: ['draft', 0, 1] }, {}, 'Marta Alves', '2 h ago'),
|
||||
mk('a2', 'Heat pumps for stone farmhouses', { en: ['pub', 0, 2], 'en-GB': ['pub', 0, 1], fr: ['draft', 0, 1], de: ['pub', 0, 1], ja: ['draft', 0, 1] }, {}, 'Luis Ferrer', 'Yesterday'),
|
||||
mk('a3', 'Sourcing reclaimed oak beams', { en: ['draft', 0, 1], 'en-GB': ['draft', 0, 1] }, {}, 'Camille Roche', '14 Oct')
|
||||
]
|
||||
ATELIER[0].seo = 1; ATELIER[2].seo = 1
|
||||
const ARCHIVE = [['Como restaurar azulejos antigos', 'pub', 'Marta Alves', '3 days ago'], ['Guia de cal e areia', 'pub', 'Camille Roche', '14 Oct'], ['Pintura para fachadas húmidas', 'draft', 'Luis Ferrer', '13 Oct'], ['Orçamento de uma reabilitação', 'pub', 'Marta Alves', '12 Oct'], ['Telhados de telha cerâmica', 'pub', 'Camille Roche', '10 Oct']].map((r, i) => mk('r' + (i + 1), r[0], { 'pt-BR': [r[1], 0, 1] }, {}, r[2], r[3]))
|
||||
const NOTES = [['Site visit notes', 'Luis Ferrer', '3 days ago'], ['Supplier call summary', 'Marta Alves', '14 Oct'], ['Tile samples list', 'Camille Roche', '10 Oct']].map((r, i) => mk('n' + (i + 1), r[0], {}, {}, r[1], r[2]))
|
||||
const SITES = {
|
||||
journal: { name: 'Journal', stories: STORIES, langs: ['en', 'fr', 'de', 'ar'], disabled: ['es', 'it'], def: 'en', size: 5, locale: 'en' },
|
||||
press: { name: 'Press', stories: PRESS, langs: ['en', 'fr'], disabled: [], def: 'en', size: 5, locale: 'en' },
|
||||
atelier: { name: 'Atelier', stories: ATELIER, langs: ['en', 'en-GB', 'fr', 'de', 'pt-BR', 'zh-Hans', 'ja', 'ar'], disabled: [], def: 'en', size: 5, locale: 'en' },
|
||||
archive: { name: 'Archive', stories: ARCHIVE, langs: ['pt-BR'], disabled: [], def: 'pt-BR', size: 5, locale: 'pt-BR' },
|
||||
notes: { name: 'Notes', stories: NOTES, langs: [], disabled: [], def: null, size: 5, locale: null },
|
||||
gallery: { name: 'Gallery', stories: [], langs: ['en-GB'], disabled: [], def: 'en-GB', size: 5, locale: 'en-GB' }
|
||||
}
|
||||
const pl = (n, w) => n + ' ' + w + (n === 1 ? '' : 's')
|
||||
const tt = (s, k) => s.tr[k] || s.title
|
||||
const repKey = (s, def) => s.v[def] ? def : Object.keys(s.v).sort()[0]
|
||||
const view = o => {
|
||||
const site = o.site, en = site.langs, lang = o.lang || null, create = o.create || en, st = o.status, au = o.author, q = (o.q || '').toLowerCase(), seo = o.seo, size = o.size || site.size
|
||||
const ss = site.stories.map(s => { const v = {}; Object.keys(s.v).forEach(k => { if (!o.read || o.read.includes(k)) v[k] = s.v[k] }); return { ...s, v } }).filter(s => Object.keys(s.v).length && (!o.scope || o.scope(s)))
|
||||
const pass = (s, vs, ks) => (!st || vs.some(x => x[0] === st)) && (!au || s.by === au) && (!seo || s.seo) && (!q || ks.some(k => tt(s, k).toLowerCase().includes(q)))
|
||||
const all = s => pass(s, Object.values(s.v), Object.keys(s.v))
|
||||
let tabs, rowsBy, LN = lang ? LANG_NAMES[lang] : ''
|
||||
if (lang) {
|
||||
const inL = ss.filter(s => s.v[lang] && pass(s, [s.v[lang]], [lang])), miss = ss.filter(s => !s.v[lang] && all(s))
|
||||
rowsBy = { all: inL, pub: inL.filter(s => s.v[lang][0] === 'pub'), draft: inL.filter(s => s.v[lang][0] === 'draft'), review: inL.filter(s => s.v[lang][1]), missing: miss }
|
||||
tabs = [['All', 'all'], ['Published', 'pub'], ['Drafts', 'draft'], ['Needs review', 'review'], ['Not in ' + LN, 'missing']]
|
||||
} else {
|
||||
const b = ss.filter(all)
|
||||
rowsBy = { all: b, missing: b.filter(s => create.some(k => en.includes(k) && !s.v[k])), review: b.filter(s => Object.values(s.v).some(x => x[1])) }
|
||||
tabs = [['All articles', 'all'], ['Needs translation', 'missing'], ['Needs review', 'review']]
|
||||
}
|
||||
const tab = o.tab || 'all', rows = rowsBy[tab], total = rows.length, pages = Math.max(1, Math.ceil(total / size)), page = Math.min(o.page || 1, pages), from = total ? (page - 1) * size + 1 : 0, to = Math.min(total, page * size)
|
||||
const unit = lang ? ({ all: pl(total, LN + ' version'), pub: 'published ' + LN + ' version' + (total === 1 ? '' : 's'), draft: pl(total, LN + ' draft'), review: pl(total, LN + ' version') + ' ' + (total === 1 ? 'needs' : 'need') + ' review', missing: pl(total, 'article') + ' not in ' + LN })[tab] : ({ all: pl(total, 'article'), missing: pl(total, 'article') + ' ' + (total === 1 ? 'needs' : 'need') + ' translation', review: pl(total, 'article') + ' ' + (total === 1 ? 'needs' : 'need') + ' review' })[tab]
|
||||
return { tabs: tabs.map(t => [t[0], rowsBy[t[1]].length, t[1]]), tab, total, pages, page, size, from, to, unit, rows: rows.slice((page - 1) * size, page * size).map(s => ({ s, rep: repKey(s, site.def) })) }
|
||||
}
|
||||
const chips = (s, site, create) => {
|
||||
const en = site.langs, out = [], has = k => !!s.v[k]
|
||||
const order = [site.def].concat(en.filter(k => k !== site.def))
|
||||
order.forEach(k => { if (has(k)) out.push({ k, st: s.v[k][0], nr: s.v[k][1] }) })
|
||||
Object.keys(s.v).filter(k => !en.includes(k)).forEach(k => out.push({ k, st: s.v[k][0], nr: s.v[k][1], off: 1 }))
|
||||
order.forEach(k => { if (!has(k) && (create || en).includes(k)) out.push({ k, st: null }) })
|
||||
return out
|
||||
}
|
||||
const SOCIAL = { journal: { en: ['none'], fr: ['set', 'fr_FR'], de: ['none'], ar: ['none'], es: ['none'], it: ['none'] }, atelier: { en: ['none'], 'en-GB': ['derived', 'en_GB'], fr: ['set', 'fr_FR'], de: ['none'], 'pt-BR': ['derived', 'pt_BR'], 'zh-Hans': ['none'], ja: ['none'], ar: ['none'] } }
|
||||
const AUTHORS = Object.entries(STORIES.reduce((a, s) => (a[s.by] = (a[s.by] || 0) + 1, a), {}))
|
||||
const FIXTURE_NOTES = { Journal: '8 articles, 17 versions, page size 5; en (default), fr, de, ar enabled; es, it disabled', Press: '3 articles, en + fr, all translated', Atelier: '3 articles, 8 languages, long labels', Archive: '5 articles, one language (pt-BR kept)', Notes: '3 articles, no site locale', Gallery: 'No articles, site locale en-GB' }
|
||||
window.LOCFX = { P, STORIES, PRESS, ATELIER, ARCHIVE, NOTES, SITES, ENABLED, LANGS, LANG_NAMES, DIR, SETUP, preFr, count, view, chips, pl, tt, repKey, SOCIAL, AUTHORS, FIXTURE_NOTES, TOTALS: { stories: 8, versions: 17 } }
|
||||
})()
|
||||
Reference in new issue
Block a user