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:
IliesAllali authored and GitHub committed 2026-10-26 11:33:43 +01:00
commit a6b37bca22
7 files changed
+67 -11

No files matched your search

+6 -4
View File
@@ -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)
+1 -4
View File
@@ -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>')
})
})
+2 -1
View File
@@ -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')}>
+2 -1
View File
@@ -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)
}
}
+17
View File
@@ -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
}
+17 -1
View File
@@ -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')
})
})