Ou comment arrêter de massacrer les caractères japonais avec des substr() disruptif brutaux… avec détection automatique de la langue et points de suspension adaptés à chaque culture.
SEO & accessibilité : Cette méthode produit du HTML sémantique qui respecte les bonnes pratiques pour le référencement. Le texte complet reste présent dans le DOM (via sr-only), ce qui permet aux moteurs de recherche d'indexer l'intégralité du contenu. Les points de suspension générés via CSS (::after) n'interfèrent pas avec le crawl. Pour les langues RTL, l'attribut dir aide Google et autres moteurs à comprendre correctement la direction du texte, améliorant ainsi le référencement multilingue.
Contexte
Vous avez un texte trop long, vous voulez le tronquer. Facile non ? Sauf que :
- Vos utilisateurs avec lecteurs d'écran veulent le texte complet
- Les arabophones lisent de droite à gauche (et non, inverser la chaîne ne règle rien)
- Le japonais n'a pas d'espaces entre les mots
- Les points de suspension ne sont pas les mêmes partout (…, ・・・, ...)
Bienvenue dans le monde merveilleux de l'i18n.
Cette méthode statique s'intègre dans une classe utilitaire PHP (typiquement TextHelper, StringUtils ou AccessibilityHelper). Elle fait partie de ces petits outils qu'on ajoute à son projet pour éviter de réécrire le même code partout.
class TextHelper
{
public static function truncate(...) { /* ... */ }
private static function detectLanguage(...) { /* ... */ }
private static function getLanguageConfig(...) { /* ... */ }
}
Nécessite PHP 7.4 et supèrieur.
Limites assumées
- La détection de langue est basique. Pour du texte multilingue, passez
$langexplicitement. - Pas de gestion des ligatures complexes (arabe contextuel, devanagari...) — mais le navigateur s'en charge.
- Si vous avez besoin de plus, utilisez une lib i18n comme
intlousymfony/intl.
Une méthode qui tronque du texte proprement :
- Détection automatique de la langue
- Respect de l'accessibilité (lecteurs d'écran)
- Support RTL (arabe, hébreu)
- Support des langues sans espaces (CJK, thaï)
- Points de suspension adaptés
- Double mode : par caractères OU par mots
TextHelper::truncate()
Une méthode qui gère tout ça proprement, sans vous faire implémenter 47 bibliothèques de détection linguistique.
Signature
public static function truncate(
string $content,
int $length = 30,
bool $byWords = false,
?string $lang = null
): string
Paramètres
$content — Votre texte. Du simple ASCII au kanji le plus exotique, on prend tout.
$length — La limite. 30 par défaut parce que c'est un chiffre rond et que personne n'a envie de lire vos pavés.
$byWords — Le choix crucial : tronquer par nombre de caractères ou par nombre de mots ?
false(défaut) : limite en caractères. "Bonjour tout le monde" avec$length = 10donne "Bonjour to…"true: limite en mots. "Bonjour tout le monde" avec$length = 2donne "Bonjour tout…"
Note : pour les langues sans espaces (chinois, japonais, thaï), le mode "mots" est automatiquement désactivé car... y'a pas de mots séparables.
$lang — Code ISO 639-1 ('fr', 'ar', 'ja'...). Si vous le laissez à null, la méthode joue au détective avec detectLanguage().
Fonctionnement
1. Détection de la langue
Si vous ne spécifiez pas la langue, la méthode analyse le contenu avec des regex Unicode :
private static function detectLanguage(string $content): string
{
if (preg_match('/[\x{0600}-\x{06FF}]/u', $content)) {
return 'ar'; // Arabe
}
if (preg_match('/[\x{4E00}-\x{9FFF}]/u', $content)) {
return 'zh'; // Chinois
}
// ... etc
}
C'est pas du machine learning, mais ça marche très bien pour 95% des cas. Pour les 5% restants, passez le code langue explicitement.
2. Configuration selon la langue
Chaque langue a ses spécificités via getLanguageConfig() :
- Ellipsis :
…en français/anglais,...en japonais, etc. - RTL :
truepour l'arabe et l'hébreu - Use spaces :
falsepour le CJK et le thaï (pas d'espaces entre les mots)
3. Troncature intelligente
Mode mots (si activé ET si la langue utilise des espaces) :
$words = preg_split('/\s+/u', $content, -1, PREG_SPLIT_NO_EMPTY);
$visible_part = implode(' ', array_slice($words, 0, $length));
$hidden_part = ' ' . implode(' ', array_slice($words, $length));
Mode caractères (par défaut ou forcé pour CJK/thaï) :
$visible_part = mb_substr($content, 0, $length);
$hidden_part = mb_substr($content, $length);
Notez le mb_substr() : parce que substr() découpe les octets, pas les caractères. Un emoji vous remercie.
4. HTML accessible
Le rendu final ressemble à ça :
<span dir="rtl">
Texte visible
<span aria-hidden="true" data-text-after="…"></span>
<span class="sr-only">texte caché</span>
</span>
dir="rtl": ajouté automatiquement pour l'arabe/hébreuaria-hidden="true": les points de suspension sont masqués aux lecteurs d'écranclass="sr-only": le reste du texte est accessible uniquement aux lecteurs d'écran
Utilisation
Cas classique (pour nous, ici) : texte français par caractères.
$text = "Les développeurs qui n'utilisent pas mb_substr méritent des bugs jusqu'à la retraite.";
echo TextHelper::truncate($text, 30);
// Affiche : "Les développeurs qui n'utilis…" + texte caché
Texte français par mots.
$text = "Les développeurs qui n'utilisent pas mb_substr méritent des bugs Unicode.";
echo TextHelper::truncate($text, 5, true);
// Affiche : "Les développeurs qui n'utilisent pas…" + texte caché
Texte arabe (RTL).
$text = "النص العربي يُقرأ من اليمين إلى اليسار";
echo TextHelper::truncate($text, 20, false, 'ar');
// Génère un span avec dir="rtl" et utilise les bons points de suspension
Texte japonais.
$text = "日本語にはスペースがないので文字数で切るしかない";
echo TextHelper::truncate($text, 15, true, 'ja');
// byWords est ignoré, on tronque par caractères (pas d'espaces en japonais)
Pourquoi c'est mieux que substr() + '...'
| Le vieux code | Cette méthode |
|---|---|
| Coupe en plein milieu d'un emoji ❤️ | Respecte les caractères multi-octets |
| Points de suspension hardcodés | Adaptés à la langue |
| Texte perdu pour les lecteurs d'écran | Accessible via sr-only |
| RTL cassé | Direction gérée automatiquement |
| Coupe des mots en deux | Option de troncature par mots |
Code complet
Implémentation complète à copier dans une classe utilitaire :
<?php
class TextHelper
{
/**
* Tronque une chaîne de caractères à une longueur spécifiée,
* en ajoutant des points de suspension accessibles et en masquant
* le surplus de la chaîne dans un span en sr-only.
*
* @param string $content La chaîne de caractères à tronquer.
* @param int $length La longueur maximale après laquelle tronquer (par défaut 30).
* @param bool $byWords Si true, tronque au nombre de mots, sinon au nombre de caractères (par défaut false).
* @param string|null $lang Code langue ISO 639-1 (ex: 'fr', 'en', 'ja', 'ar'). Si null, détection auto ou par défaut.
* @return string Le contenu tronqué avec les éléments d'accessibilité.
*/
public static function truncate(
string $content,
int $length = 30,
bool $byWords = false,
?string $lang = null
): string
{
// Détection automatique de la langue si non spécifiée
if ($lang === null) {
$lang = self::detectLanguage($content);
}
// Configuration selon la langue
$config = self::getLanguageConfig($lang);
$ellipsis = $config['ellipsis'];
$isRTL = $config['rtl'];
$useSpaces = $config['use_spaces'];
// Pour les langues sans espaces (CJK, thaï), forcer le mode caractères
if (!$useSpaces && $byWords) {
$byWords = false;
}
if ($byWords) {
// Mode : troncature par nombre de mots
$words = preg_split('/\s+/u', $content, -1, PREG_SPLIT_NO_EMPTY);
$word_count = count($words);
if ($word_count <= $length) {
return $content;
}
$visible_part = implode(' ', array_slice($words, 0, $length));
$hidden_part = ' ' . implode(' ', array_slice($words, $length));
} else {
// Mode : troncature par nombre de caractères
if (mb_strlen($content) <= $length) {
return $content;
}
$visible_part = mb_substr($content, 0, $length);
$hidden_part = mb_substr($content, $length);
}
// Construit le résultat avec les balises d'accessibilité et la direction du texte
$dirAttr = $isRTL ? ' dir="rtl"' : '';
$output = '<span' . $dirAttr . '>';
$output .= $visible_part;
$output .= '<span aria-hidden="true" data-text-after="' . $ellipsis . '"></span>';
$output .= '<span class="sr-only">' . $hidden_part . '</span>';
$output .= '</span>';
return $output;
}
/**
* Détecte la langue du contenu (méthode simplifiée).
*
* @param string $content Le contenu à analyser.
* @return string Code langue détecté (par défaut 'en').
*/
private static function detectLanguage(string $content): string
{
// Détection basique par plages Unicode
if (preg_match('/[\x{0600}-\x{06FF}\x{0750}-\x{077F}\x{08A0}-\x{08FF}]/u', $content)) {
return 'ar'; // Arabe
}
if (preg_match('/[\x{0590}-\x{05FF}]/u', $content)) {
return 'he'; // Hébreu
}
if (preg_match('/[\x{4E00}-\x{9FFF}]/u', $content)) {
return 'zh'; // Chinois
}
if (preg_match('/[\x{3040}-\x{309F}\x{30A0}-\x{30FF}]/u', $content)) {
return 'ja'; // Japonais
}
if (preg_match('/[\x{AC00}-\x{D7AF}]/u', $content)) {
return 'ko'; // Coréen
}
if (preg_match('/[\x{0E00}-\x{0E7F}]/u', $content)) {
return 'th'; // Thaï
}
// Par défaut : langue occidentale (français/anglais/cyrillique/etc.)
return 'en';
}
/**
* Retourne la configuration spécifique à chaque langue.
*
* @param string $lang Code langue ISO 639-1.
* @return array Configuration (ellipsis, rtl, use_spaces).
*/
private static function getLanguageConfig(string $lang): array
{
$configs = [
'ar' => ['ellipsis' => '...', 'rtl' => true, 'use_spaces' => true],
'he' => ['ellipsis' => '...', 'rtl' => true, 'use_spaces' => true],
'zh' => ['ellipsis' => '...', 'rtl' => false, 'use_spaces' => false],
'ja' => ['ellipsis' => '...', 'rtl' => false, 'use_spaces' => false],
'ko' => ['ellipsis' => '...', 'rtl' => false, 'use_spaces' => false],
'th' => ['ellipsis' => '...', 'rtl' => false, 'use_spaces' => false],
];
// Configuration par défaut (langues occidentales)
return $configs[$lang] ?? ['ellipsis' => '…', 'rtl' => false, 'use_spaces' => true];
}
}
CSS associée
Pour que data-text-after fonctionne :
[data-text-after]::after {
content: attr(data-text-after);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
That's all, Folks !