mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-06 23:32:08 +00:00
Merge pull request #159 from IliesAllali/fix/noms-monde
Noms en chinois, arabe, hébreu : une seule initiale, lieu et surnom isolés du sens de la ligne
This commit is contained in:
7 files changed
+67
-11
No files matched your search
@@ -21,6 +21,7 @@ import DateField from './DateField.jsx'
|
||||
import { runOnce } from '../utils/saveOnLeave.js'
|
||||
import { normalizeSex, SEX_CHOICES } from '../utils/personSex.js'
|
||||
import { ProposalBanner } from './ProposalCard.jsx'
|
||||
import { initialsOf } from '../utils/initial.js'
|
||||
|
||||
// Aucune modification en cours (référence stable)
|
||||
const NO_EDITS = Object.freeze({})
|
||||
@@ -602,19 +603,20 @@ const PersonCard = ({
|
||||
const visualMedias = personMedias.filter((m) => m.type === 'photo' || m.type === 'video')
|
||||
const otherMedias = personMedias.filter((m) => !['citation', 'audio', 'photo', 'video'].includes(m.type) || (m.type === 'citation' && m !== quoteMedia))
|
||||
|
||||
const initials = `${person.firstName?.[0] || ''}${person.lastName?.[0] || ''}` || '?'
|
||||
const initials = initialsOf(person)
|
||||
const lifeLine = (() => {
|
||||
const born = person.birthYear || (person.birthDate ? String(person.birthDate).slice(0, 4) : '')
|
||||
const died = person.deathYear || (person.deathDate ? String(person.deathDate).slice(0, 4) : '')
|
||||
const parts = []
|
||||
if (born) parts.push(`${born}${person.birthPlace ? `, ${formatPlace(person.birthPlace)}` : ''}`)
|
||||
// <bdi> : un lieu en arabe ou en hébreu ne retourne pas l'ordre de la ligne (« 1980, الدار البيضاء · 46 ans »)
|
||||
if (born) parts.push(person.birthPlace ? <>{born}, <bdi>{formatPlace(person.birthPlace)}</bdi></> : born)
|
||||
if (!person.isAlive) parts.push(died || '?')
|
||||
return parts.join(' – ')
|
||||
return parts.length ? parts.reduce((line, part) => <>{line} – {part}</>) : null
|
||||
})()
|
||||
const ageLabel = age === null ? null : person.isAlive ? t('{age} ans', { age }) : t('{age} ans au décès', { age })
|
||||
// Une seule ligne sous le nom : surnom, repères de vie, âge, séparés par un point médian
|
||||
const metaItems = [
|
||||
person.nickname && { key: 'nick', node: <em className="pc-nick">{person.nickname}</em>, nowrap: true },
|
||||
person.nickname && { key: 'nick', node: <em className="pc-nick"><bdi>{person.nickname}</bdi></em>, nowrap: true },
|
||||
lifeLine && { key: 'life', node: lifeLine },
|
||||
ageLabel && { key: 'age', node: ageLabel, nowrap: true },
|
||||
].filter(Boolean)
|
||||
|
||||
@@ -5,6 +5,7 @@ import PzBusy from './PzBusy.jsx'
|
||||
import { t } from '../i18n/index.js'
|
||||
import { matchPhotoFiles } from '../utils/photoImport.js'
|
||||
import { materializeFile } from '../utils/zipReader.js'
|
||||
import { initialsOf } from '../utils/initial.js'
|
||||
|
||||
function withEmphasis(text) {
|
||||
const [before, emphasized = '', after = ''] = text.split(/<\/?em>/)
|
||||
@@ -21,10 +22,6 @@ function plural(n, one, many, vars = {}) {
|
||||
return n === 1 ? t(one, vars) : t(many, { n, ...vars })
|
||||
}
|
||||
|
||||
function initialsOf(person) {
|
||||
return `${person.firstName?.[0] || ''}${person.lastName?.[0] || ''}`.toUpperCase() || '?'
|
||||
}
|
||||
|
||||
// Dossier où le logiciel d'origine rangeait la première photo citée, pour aider à le retrouver
|
||||
function folderOf(filePath) {
|
||||
const parts = String(filePath || '').split(/([\\/])/)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import PhotoStartPanel from './PhotoStartPanel.jsx'
|
||||
|
||||
// Les visages à compléter, sans aperçu : chaque portrait vide montre les initiales de la personne
|
||||
describe('PhotoStartPanel, visages sans photo', () => {
|
||||
it('affiche les initiales, une seule pour un nom chinois', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<PhotoStartPanel
|
||||
visible
|
||||
personsImported={2}
|
||||
faces={[
|
||||
{ id: 'a', firstName: 'Jeanne', lastName: 'Duval', birthYear: 1950 },
|
||||
{ id: 'b', firstName: '小明', lastName: '王', birthYear: 1978 },
|
||||
]}
|
||||
onClose={() => {}}
|
||||
/>,
|
||||
)
|
||||
expect(html).toContain('>JD</span>')
|
||||
expect(html).toContain('>王</span>')
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import { t, dateLocale } from '../i18n/index.js'
|
||||
import { firstNameLabel } from '../utils/personName.js'
|
||||
import PzBusy from './PzBusy'
|
||||
import './ProposalCard.css'
|
||||
import { initialsOf } from '../utils/initial.js'
|
||||
|
||||
// Proposition pas encore relue, ouverte depuis son fantôme sur l'arbre (1.5.1). Qui gère l'arbre l'accepte ou la
|
||||
// refuse sur place ; la famille y lit qu'elle attend. Une proposition se relit en entier : « Voir le détail »
|
||||
@@ -73,7 +74,7 @@ export default function ProposalCard({ person, summary = '', review = null, onCl
|
||||
const ghost = person?.ghost
|
||||
const years = [person?.birthYear, person?.deathYear].filter(Boolean).join(' – ')
|
||||
const name = [firstNameLabel(person), person?.lastName].filter(Boolean).join(' ')
|
||||
const initials = `${(person?.firstName || '?')[0]}${(person?.lastName || '')[0] || ''}`.toUpperCase()
|
||||
const initials = initialsOf(person)
|
||||
return (
|
||||
<div className="person-card proposal-card" role="dialog" aria-label={t('Proposition : {name}', { name })}>
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon pc-close" onClick={onClose} aria-label={t('Fermer')}>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { getEntranceProgress, drawPartialBezier } from './utils'
|
||||
import { fitNickname } from '../../utils/nickname.js'
|
||||
import { firstNameLabel, isFirstNameUnknown } from '../../utils/personName.js'
|
||||
import { initialsOf } from '../../utils/initial.js'
|
||||
|
||||
// Largeur maximale du surnom sur le nœud (px canevas) : au-delà, coupé avec …
|
||||
const NICKNAME_MAX_W = 150
|
||||
@@ -1059,7 +1060,7 @@ function drawPersonImage(ctx, img, person, x, y, w, h) {
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillStyle = COLORS.text
|
||||
ctx.font = '400 16px "Newsreader", Georgia, serif'
|
||||
ctx.fillText((person.firstName[0] || '') + (person.lastName[0] || ''), 0, 0)
|
||||
ctx.fillText(initialsOf(person, ''), 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,3 +9,20 @@ export function initialOf(text, fallback = '?') {
|
||||
const first = segmenter ? segmenter.segment(value)[Symbol.iterator]().next().value.segment : Array.from(value)[0]
|
||||
return first.toUpperCase()
|
||||
}
|
||||
|
||||
// Initiales d'une personne : prénom puis nom (« JD »). Une écriture sans majuscules (chinois, japonais,
|
||||
// coréen, arabe, hébreu…) n'a pas d'initiales : deux caractères collés s'y lisent comme un mot (« 小王 »,
|
||||
// « فا »). On garde alors un seul caractère : le nom de famille pour le chinois, le japonais et le coréen,
|
||||
// qui l'écrivent en premier, le prénom sinon.
|
||||
const EAST_ASIAN = /[\p{Script=Han}\p{Script=Hangul}\p{Script=Hiragana}\p{Script=Katakana}]/u
|
||||
const caseless = (c) => /\p{L}/u.test(c) && c.toUpperCase() === c.toLowerCase()
|
||||
|
||||
export function initialsOf(person, fallback = '?') {
|
||||
const first = initialOf(person?.firstName, '')
|
||||
const last = initialOf(person?.lastName, '')
|
||||
if (caseless(first) || caseless(last)) {
|
||||
const eastAsian = EAST_ASIAN.test(`${person?.firstName ?? ''}${person?.lastName ?? ''}`)
|
||||
return (eastAsian ? last || first : first || last) || fallback
|
||||
}
|
||||
return `${first}${last}` || fallback
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { initialOf } from './initial.js'
|
||||
import { initialOf, initialsOf } from './initial.js'
|
||||
|
||||
describe('initialOf', () => {
|
||||
it('donne la première lettre en majuscule', () => {
|
||||
@@ -18,3 +18,19 @@ describe('initialOf', () => {
|
||||
expect(initialOf(null)).toBe('?')
|
||||
})
|
||||
})
|
||||
|
||||
describe('initialsOf', () => {
|
||||
it('prénom puis nom', () => {
|
||||
expect(initialsOf({ firstName: 'jeanne', lastName: 'Duval' })).toBe('JD')
|
||||
expect(initialsOf({ firstName: 'Наталья', lastName: 'Иванова' })).toBe('НИ')
|
||||
expect(initialsOf({ firstName: 'Élodie', lastName: '' })).toBe('É')
|
||||
expect(initialsOf({ firstName: '', lastName: '' })).toBe('?')
|
||||
})
|
||||
|
||||
it('un seul caractère pour une écriture sans majuscules', () => {
|
||||
expect(initialsOf({ firstName: '小明', lastName: '王' })).toBe('王')
|
||||
expect(initialsOf({ firstName: '小明', lastName: '' })).toBe('小')
|
||||
expect(initialsOf({ firstName: 'فاطمة', lastName: 'الزهراء' })).toBe('ف')
|
||||
expect(initialsOf({ firstName: 'Sarah', lastName: 'לוי' })).toBe('S')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user