Intégration de la lib https://github.com/commerceguys/intl + fonctions SPIP facilitatrices autour.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

410 lines
14 KiB

<?php
// Sécurité
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
/**
* Raccourci vers filtre_montant_formater_dist() qui est surchargeable
*
* @note
* Fonction déportée dans la fonction surchargeable `filtre_montant_formater_dist`.
*
* @uses filtre_montant_formater_dist
*/
function montant_formater($montant, $options = array()) {
include_spip('inc/filtres');
$fonction_formater = chercher_filtre('montant_formater');
return $fonction_formater($montant, $options);
}
/**
* Formater un montant (un nombre + une devise)
*
* Le montant retourné respecte les règles d'affichages propres à chaque langue et devise :
* nombre de décimales, virgules et/ou points, emplacement de la devise, etc.
* Il inclut les balises meta pour les micro-données.
*
* L'option `currency_display` permet d'avoir un format spécifique aux factures.
* L'option `float_only` permet d'avoir le nombre flottant arrondi selon la devise.
*
* @note
* Nécessite soit l'extension bcmath, soit l'extension intl.
*
* @example montant_formater($montant, array('currency'=>'EUR', 'locale'=>'fr-CA'))
*
* @see https://github.com/commerceguys/intl/blob/master/src/Formatter/CurrencyFormatterInterface.php#L8
* @see https://www.php.net/manual/fr/numberformatter.formatcurrency.php
*
* @uses intl_devise_defaut
* @uses intl_locale_defaut
* @uses intl_devise_info
* @uses intl_langue_vers_locale
* @uses intl_filtrer_options_formater
* @uses intl_alias_options_formater
*
* @param float $montant
* Valeur du montant à formater
* @param array $options
* Tableau d'options :
* - markup : (String|Boolean) pour encapsuler ou pas dans un <span>
* Défaut: true
* - class : (String) nom de la classe parente pour encapsuler
* Défaut : montant
* - currency : (String) devise, code alphabétique à 3 lettres.
* Défaut : celle configurée
* - locale : (String) identifiant d'une locale (fr-CA) ou code de langue SPIP (fr_tu)
* - style : (String) standard | accounting.
* Défaut : standard
* - use_grouping : (Bool) grouper les séparateurs.
* Défaut : true
* - rounding_mode : constante PHP_ROUND_ ou `none`.
* Défaut : PHP_ROUND_HALF UP
* - minimum_fraction_digits : (Int)
* Défaut : fraction de la devise.
* - maximum_fraction_digits : (Int)
* Défaut : fraction de la devise.
* - currency_display : (String) symbol | code | none.
* Défaut : code
* @return string|float
* Retourne une chaine contenant le montant formaté avec une devise, encapsulée dans un <span>
*/
function filtre_montant_formater_dist($montant, $options = array()) {
intl_loader();
// S'assurer d'avoir un nombre flottant
$montant = floatval(str_replace(array(',', ' '), array('.', ''), $montant));
// Devise à utiliser
$devise = (!empty($options['currency']) ? $options['currency'] : intl_devise_defaut());
// Locale à utiliser
$locale = (!empty($options['locale']) ? $options['locale'] : intl_locale_defaut());
$locale = intl_langue_vers_locale($locale);
// Options (celles propres au formatter + diverses)
$options_defaut = array(
'locale' => $locale,
'currency_display' => 'code', // pour l'accessibilité
'markup' => true, // encapsuler
);
$options = array_merge($options_defaut, is_array($options) ? $options : array());
// 1) De préférence, on utilise la librairie Intl de Commerceguys
if (extension_loaded('bcmath')) {
// Éviter les exceptions invalid argument.
$options_formatter = intl_normaliser_options_formatter($options);
$numberFormatRepository = new CommerceGuys\Intl\NumberFormat\NumberFormatRepository;
$currencyRepository = new CommerceGuys\Intl\Currency\CurrencyRepository;
$currencyFormatter = new CommerceGuys\Intl\Formatter\CurrencyFormatter($numberFormatRepository, $currencyRepository);
$montant_formate = $currencyFormatter->format($montant, $devise, $options_formatter);
// 2) Sinon on se rabat sur la librairie Intl PECL
} elseif (extension_loaded('intl')) {
$currencyFormatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
if ($options['currency_display'] !== 'none') {
$montant_formate = $currencyFormatter->formatCurrency($montant, $devise);
}
else {
$montant_formate = trim($currencyFormatter->formatCurrency($montant, ''), " ¤");
}
// 3) En dernier recours on fait le formatage du pauvre
} else {
$montant_nombre = str_replace('.', ',', $montant);
$montant_formate = $montant_nombre . ($options['currency_display'] !== 'none' ? '&nbsp;' . $devise : '');
}
// Enfin, on encapsule le tout
if (!empty($options['markup'])) {
$classe = (!empty($options['class']) ? $options['class'] : null);
// S'assurer que le montant soit arrondi selon la devise, même si c'est en principe fait en amont
$arrondi_devise = intval(intl_devise_info($devise, 'fraction'));
$montant = round($montant, $arrondi_devise);
$montant_formate = intl_ajouter_markup($montant_formate, $montant, $devise, $classe);
}
return $montant_formate;
}
/**
* Encapsule un montant dans des <span> en isolant la devise, en mode BEM.
*
* @note
* La devise peut parfois être incluse dans le nombre, ex. : -AED123
* C'est pourquoi on encapsule uniquement la devise dans un span, mais pas le nombre,
* sinon cela ferait parfois une imbrication non voulue.
*
* @example
* ````
* <span class="montant" data-montant-nombre="3.14" data-montant-devise="EUR">
* 3,14 <span class="montant__devise">EUR</span>
* <meta itemprop="price" content="3.14" />
* <meta itemprop="priceCurrency" content="EUR" />
* </span>
* ````
*
* @param string $montant_formate
* Montant formaté avec éventuellement la devise ou le symbole
* @param float $montant
* Montant sans formatage
* @param string $devise
* Code alphabétique à 3 lettres
* @param string $classe
* Classe parente à utiliser, défaut = 'montant'
* @return string
*/
function intl_ajouter_markup($montant_formate, $montant, $devise, $classe = '') {
// Les classes BEM
$classe = (!empty($classe) ? $classe : 'montant');
$classe_devise = "${classe}__devise";
// D'abord on isole la devise et on l'encapsule dans un span (mais pas le nombre, cf. @note)
$cherche_devise = '/[^\d\-\.\,\(\)\s x{00a0}]+/u';
$remplace_devise = "<span class=\"$classe_devise\">$0</span>";
$montant_formate = preg_replace($cherche_devise, $remplace_devise, $montant_formate);
// Puis on encpasule le tout dans un span, avec les microdatas
$montant_formate =
"<span class=\"$classe\" data-montant-nombre=\"$montant\" data-montant-devise=\"$devise\">"
. "\n\t$montant_formate"
. "\n\t<meta itemprop=\"price\" content=\"$montant\" />"
. "\n\t<meta itemprop=\"priceCurrency\" content=\"$devise\" />"
. "\n</span>";
return $montant_formate;
}
/**
* Liste les devises et les informations associées
*
* @uses intl_devise_info()
*
* @return Array
* Tableau associatif avec les codes alphabétiques en clés et les infos en sous-tableaux
*/
function intl_lister_devises() {
intl_loader();
$devises = array();
$currencyRepository = new CommerceGuys\Intl\Currency\CurrencyRepository;
$codes_devises = $currencyRepository->getList();
foreach ($codes_devises as $code => $nom) {
$devises[$code] = intl_devise_info($code);
}
return $devises;
}
/**
* Liste les langues avec leur identifiant de locale.
*
* @see https://www.php.net/manual/fr/class.locale.php
*
* @return Array
* Tableau associatif : locale => nom
*/
function intl_lister_langues() {
intl_loader();
$langues = array();
$languageRepository = new CommerceGuys\Intl\Language\LanguageRepository;
$repo_locales = $languageRepository->getlist();
// Prendre la langue du visiteur pour les noms
$langue_spip = $GLOBALS['spip_lang'];
$locale_visiteur = intl_langue_vers_locale($langue_spip);
foreach ($repo_locales as $locale => $nom) {
$language = $languageRepository->get($locale, $locale_visiteur);
$langues[$locale] = $language->getName();
}
return $langues;
}
/**
* Renvoie une ou toutes les infos sur une devise
*
* @param string $code
* Code alphabétique à 3 lettres de la devise
* @param string $info
* Info précise éventuelle :
* - nom : nom de la devise
* - code : code alphabétique (remis au cas où)
* - code_num : code numérique
* - symbole : symbole associé
* - fraction : fraction pour passer à l'unité inférieure (centimes et cie)
* - langue : code de langue utilisée
* @return string|array
*/
function intl_devise_info($code, $info = '') {
intl_loader();
// Langue du visiteur pour les noms
$langue_spip = $GLOBALS['spip_lang'];
$locale_visiteur = intl_langue_vers_locale($langue_spip);
$currencyRepository = new CommerceGuys\Intl\Currency\CurrencyRepository;
$devise = $currencyRepository->get($code, $locale_visiteur);
$infos = array(
'code' => $code,
'code_num' => $devise->getNumericCode(),
'nom' => $devise->getName(),
'fraction' => $devise->getFractionDigits(),
'symbole' => $devise->getSymbol(),
'locale' => $devise->getLocale(),
);
// Par défaut on retourne tout donc un tableau
$retour = $infos;
// Sauf si on demande une info précise
if ($info) {
$retour = (isset($infos[$info]) ? $infos[$info] : null);
}
return $retour;
}
/**
* Retourne la devise par défaut.
*
* Celle configurée, sinon des euros
*
* @return String
* Code alphabétique à 3 lettres
*/
function intl_devise_defaut() {
include_spip('inc/config');
// Par défaut celle configurée
if ($devise_config = lire_config('intl/devise_defaut')) {
$devise = $devise_config;
// Sinon des euros
} else {
$devise = 'EUR';
}
return $devise;
}
/**
* Retourne la locale d'après la langue du contexte
*
* @return String
* Identifiant de la locale
*/
function intl_locale_defaut() {
include_spip('inc/config');
$langue_spip = $GLOBALS['spip_lang'];
$locales_config = lire_config('intl/locales', array());
// Normalement l'admin a configuré la locale correspondante à chaque code langue de spip.
// Sinon tant pis, on donne juste le code pays tiré du code langue de spip.
$locale = $locales_config[$langue_spip] ?? intl_langue_vers_locale($langue_spip);
return $locale;
}
/**
* Retourne une locale reconnue par Intl.
*
* Si c'est un code langue de spip, on ne garde que le code du pays (norme ISO 639).
*
* @see https://github.com/commerceguys/intl/blob/master/src/Language/LanguageRepository.php#L46
* @see https://blog.smellup.net/106
*
* @param string $code_langue
* @return string
*/
function intl_langue_vers_locale($code_langue) {
include_spip('inc/config');
$locale = $code_langue;
$is_langue_spip = in_array($code_langue, explode(',', lire_config('langues_proposees')));
if ($is_langue_spip) {
// Extraire le code pays pour avoir la locale "générale" : fr_tu → fr
$locale = strtolower(strtok($code_langue, '_'));
// Exceptions : certains codes pays des langues de spip ne font pas partie de la liste des locales.
// On fait une correspondance manuellement en prenant la locale la plus proche.
// (ça n'indique pas que ce sont des langues identiques, mais suffisamment proches pour le formatage des montants)
$exceptions = array(
'oc' => 'fr', // occitan
'ay' => 'ayr', // aymara
'co' => 'fr', // corse
'cpf' => 'fr', // créole et pidgins (rcf)
'fon' => '', // fongbè
'roa' => 'pdc', // langues romanes
'pt_br' => 'pt', // portugais
'pt' => 'pt-PT' // portugais du Portugal
);
if (!empty($exceptions[$locale])) {
$locale = $exceptions[$locale];
}
}
return $locale;
}
/**
* Fonction privée pour filtrer le tableau d'options du formatter
*
* Retire les options inconnues et typecaste les valeurs pour éviter les exceptions invalid argument.
*
* Le tableau d'options peut être issu d'un squelette,
* et dans ce cas par défaut les valeurs sont des chaînes de texte à défaut de |filtre ou de #EVAL.
*
* @param array $valeurs
* @return array
*/
function intl_normaliser_options_formatter($options) {
$options_valides = array(
'locale',
'style',
'use_grouping',
'rounding_mode',
'minimum_fraction_digits',
'maximum_fraction_digits',
'currency_display',
);
if (is_array($options)) {
foreach ($options as $k => $v) {
// option inconnue, chaine vide ou null : on retire la valeur
if (!in_array($k, $options_valides) or is_null($v) or $v == '') {
unset($options[$k]);
// nombre flottant / entier
} elseif (is_numeric($v)) {
if (intval($v) == $v) {
$options[$k] = intval($v);
} else {
$options[$k] = floatval($v);
}
// booléens
} elseif (in_array($v, array('true', 'oui'))) {
$options[$k] = true;
} elseif (in_array($v, array('false', 'non'))) {
$options[$k] = false;
}
}
}
return $options;
}
/**
* Autoloader
* @throws Exception
*/
function intl_loader() {
static $done = false;
if (!$done) {
$done = true;
require_once __DIR__ . '/vendor/autoload.php';
}
}