first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,29 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language;
/**
* Interface describing a language file parser capable of debugging a file
*
* @since 2.0.0-alpha
*/
interface DebugParserInterface extends ParserInterface
{
/**
* Parse a file and check its contents for valid structure
*
* @param string $filename The name of the file.
*
* @return string[] Array containing a list of errors
*
* @since 2.0.0-alpha
*/
public function debugFile(string $filename): array;
}

View File

@ -0,0 +1,938 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language;
/**
* Languages/translation handler class
*
* @since 1.0
*/
class Language
{
/**
* Debug language, If true, highlights if string isn't found.
*
* @var boolean
* @since 1.0
*/
protected $debug = false;
/**
* The default language, used when a language file in the requested language does not exist.
*
* @var string
* @since 1.0
*/
protected $default = 'en-GB';
/**
* An array of orphaned text.
*
* @var array
* @since 1.0
*/
protected $orphans = [];
/**
* Array holding the language metadata.
*
* @var array
* @since 1.0
*/
protected $metadata;
/**
* Array holding the language locale or boolean null if none.
*
* @var array|boolean
* @since 1.0
*/
protected $locale;
/**
* The language to load.
*
* @var string
* @since 1.0
*/
protected $lang;
/**
* A nested array of language files that have been loaded
*
* @var array
* @since 1.0
*/
protected $paths = [];
/**
* List of language files that are in error state
*
* @var array
* @since 1.0
*/
protected $errorfiles = [];
/**
* An array of used text, used during debugging.
*
* @var array
* @since 1.0
*/
protected $used = [];
/**
* Counter for number of loads.
*
* @var integer
* @since 1.0
*/
protected $counter = 0;
/**
* An array used to store overrides.
*
* @var array
* @since 1.0
*/
protected $override = [];
/**
* The localisation object.
*
* @var LocaliseInterface
* @since 2.0.0-alpha
*/
protected $localise;
/**
* LanguageHelper object
*
* @var LanguageHelper
* @since 2.0.0-alpha
*/
protected $helper;
/**
* The base path to the language folder
*
* @var string
* @since 2.0.0-alpha
*/
protected $basePath;
/**
* MessageCatalogue object
*
* @var MessageCatalogue
* @since 2.0.0-alpha
*/
protected $catalogue;
/**
* Language parser registry
*
* @var ParserRegistry
* @since 2.0.0-alpha
*/
protected $parserRegistry;
/**
* Constructor activating the default information of the language.
*
* @param ParserRegistry $parserRegistry A registry containing the supported file parsers
* @param string $path The base path to the language folder
* @param string $lang The language
* @param boolean $debug Indicates if language debugging is enabled
*
* @since 1.0
*/
public function __construct(ParserRegistry $parserRegistry, string $path, string $lang = '', bool $debug = false)
{
if (empty($path)) {
throw new \InvalidArgumentException(
'The $path variable cannot be empty when creating a new Language object'
);
}
$this->basePath = $path;
$this->helper = new LanguageHelper();
$this->lang = $lang ?: $this->default;
$this->metadata = $this->helper->getMetadata($this->lang, $this->basePath);
$this->setDebug($debug);
$this->parserRegistry = $parserRegistry;
$basePath = $this->helper->getLanguagePath($this->basePath);
$filename = $basePath . "/overrides/$lang.override.ini";
if (file_exists($filename) && $contents = $this->parse($filename)) {
if (\is_array($contents)) {
// Sort the underlying heap by key values to optimize merging
ksort($contents, SORT_STRING);
$this->override = $contents;
}
unset($contents);
}
// Grab a localisation file
$this->localise = (new LanguageFactory())->getLocalise($lang, $path);
$this->catalogue = new MessageCatalogue($this->lang);
$this->load();
}
/**
* Translate function, mimics the php gettext (alias _) function.
*
* The function checks if $jsSafe is true, then if $interpretBackslashes is true.
*
* @param string $string The string to translate
* @param boolean $jsSafe Make the result JavaScript safe
* @param boolean $interpretBackSlashes Interpret \t and \n
*
* @return string The translation of the string
*
* @see Language::translate()
* @since 1.0
* @deprecated 3.0 Use translate instead
*/
public function _($string, $jsSafe = false, $interpretBackSlashes = true)
{
trigger_deprecation(
'joomla/language',
'2.0.0',
'%s() is deprecated and will be removed in 3.0, use %s::translate() instead.',
__METHOD__,
self::class
);
return $this->translate((string) $string, (bool) $jsSafe, (bool) $interpretBackSlashes);
}
/**
* Translate function, mimics the php gettext (alias _) function.
*
* The function checks if $jsSafe is true, then if $interpretBackslashes is true.
*
* @param string $string The string to translate
* @param boolean $jsSafe Make the result JavaScript safe
* @param boolean $interpretBackSlashes Interpret \t and \n
*
* @return string The translation of the string
*
* @since 2.0.0-alpha
*/
public function translate(string $string, bool $jsSafe = false, bool $interpretBackSlashes = true): string
{
// Detect empty string
if ($string == '') {
return '';
}
$key = strtoupper($string);
if ($this->catalogue->hasMessage($key)) {
$string = $this->debug ? '**' . $this->catalogue->getMessage($key) . '**' : $this->catalogue->getMessage($key);
// Store debug information
if ($this->debug) {
$caller = $this->getCallerInfo();
if (!array_key_exists($key, $this->used)) {
$this->used[$key] = [];
}
$this->used[$key][] = $caller;
}
} else {
if ($this->debug) {
$caller = $this->getCallerInfo();
$caller['string'] = $string;
if (!array_key_exists($key, $this->orphans)) {
$this->orphans[$key] = [];
}
$this->orphans[$key][] = $caller;
$string = '??' . $string . '??';
}
}
if ($jsSafe) {
// JavaScript filter
$string = addslashes($string);
} elseif ($interpretBackSlashes) {
if (strpos($string, '\\') !== false) {
// Interpret \n and \t characters
$string = str_replace(['\\\\', '\t', '\n'], ['\\', "\t", "\n"], $string);
}
}
return $string;
}
/**
* Transliterate function
*
* This method processes a string and replaces all accented UTF-8 characters by unaccented ASCII-7 "equivalents".
*
* @param string $string The string to transliterate.
*
* @return string The transliteration of the string.
*
* @since 1.0
* @throws \RuntimeException
*/
public function transliterate($string)
{
$string = $this->localise->transliterate($string);
// The transliterate method can return false if there isn't a fully valid UTF-8 string entered
if ($string === false) {
throw new \RuntimeException('Invalid UTF-8 was detected in the string "%s"', $string);
}
return $string;
}
/**
* Returns an array of suffixes for plural rules.
*
* @param integer $count The count number the rule is for.
*
* @return string[] The array of suffixes.
*
* @since 1.0
*/
public function getPluralSuffixes($count)
{
return $this->localise->getPluralSuffixes($count);
}
/**
* Checks if a language exists.
*
* This is a simple, quick check for the directory that should contain language files for the given user.
*
* @param string $lang Language to check.
* @param string $basePath Optional path to check.
*
* @return boolean True if the language exists.
*
* @see LanguageHelper::exists()
* @since 1.0
* @deprecated 3.0 Use LanguageHelper::exists() instead
*/
public static function exists($lang, $basePath = '')
{
trigger_deprecation(
'joomla/language',
'2.0.0',
'%s() is deprecated and will be removed in 3.0, use %s::exists() instead.',
__METHOD__,
LanguageHelper::class
);
return (new LanguageHelper())->exists($lang, $basePath);
}
/**
* Loads a single language file and appends the results to the existing strings
*
* @param string $extension The extension for which a language file should be loaded.
* @param string $basePath The basepath to use.
* @param string $lang The language to load, default null for the current language.
* @param boolean $reload Flag that will force a language to be reloaded if set to true.
*
* @return boolean True if the file has successfully loaded.
*
* @since 1.0
*/
public function load($extension = 'joomla', $basePath = '', $lang = null, $reload = false)
{
$lang = $lang ?: $this->lang;
$basePath = $basePath ?: $this->basePath;
$path = $this->helper->getLanguagePath($basePath, $lang);
$internal = $extension == 'joomla' || $extension == '';
$filename = $internal ? $lang : $lang . '.' . $extension;
$filename = "$path/$filename.ini";
if (isset($this->paths[$extension][$filename]) && !$reload) {
// This file has already been tested for loading.
return $this->paths[$extension][$filename];
}
// Load the language file
return $this->loadLanguage($filename, $extension);
}
/**
* Loads a language file.
*
* This method will not note the successful loading of a file - use load() instead.
*
* @param string $filename The name of the file.
* @param string $extension The name of the extension.
*
* @return boolean True if new strings have been added to the language
*
* @see Language::load()
* @since 1.0
*/
protected function loadLanguage($filename, $extension = 'unknown')
{
$this->counter++;
$result = false;
$strings = false;
if (file_exists($filename)) {
$strings = $this->parse($filename);
}
if ($strings) {
if (\is_array($strings) && \count($strings)) {
$this->catalogue->addMessages(array_replace($strings, $this->override));
$result = true;
}
}
// Record the result of loading the extension's file.
if (!isset($this->paths[$extension])) {
$this->paths[$extension] = [];
}
$this->paths[$extension][$filename] = $result;
return $result;
}
/**
* Parses a language file.
*
* @param string $filename The name of the file.
*
* @return array The array of parsed strings.
*
* @since 1.0
*/
protected function parse($filename)
{
// Capture hidden PHP errors from the parsing.
if ($this->debug) {
// See https://www.php.net/manual/en/reserved.variables.phperrormsg.php
$php_errormsg = null;
$trackErrors = ini_get('track_errors');
ini_set('track_errors', true);
}
try {
$strings = $this->parserRegistry->get(pathinfo($filename, PATHINFO_EXTENSION))->loadFile($filename);
} catch (\RuntimeException $exception) {
// TODO - This shouldn't be absorbed
$strings = [];
}
if ($this->debug) {
// Restore error tracking to what it was before.
ini_set('track_errors', $trackErrors);
$this->debugFile($filename);
}
return \is_array($strings) ? $strings : [];
}
/**
* Debugs a language file
*
* @param string $filename Absolute path to the file to debug
*
* @return integer A count of the number of parsing errors
*
* @since 2.0.0-alpha
*/
public function debugFile(string $filename): int
{
// Make sure our file actually exists
if (!file_exists($filename)) {
throw new \InvalidArgumentException(
sprintf('Unable to locate file "%s" for debugging', $filename)
);
}
// Initialise variables for manually parsing the file for common errors.
$debug = $this->setDebug(false);
$php_errormsg = null;
$parser = $this->parserRegistry->get(pathinfo($filename, PATHINFO_EXTENSION));
if (!($parser instanceof DebugParserInterface)) {
return 0;
}
$errors = $parser->debugFile($filename);
// Check if we encountered any errors.
if (\count($errors)) {
$this->errorfiles[$filename] = $filename . ' - error(s) ' . implode(', ', $errors);
} elseif ($php_errormsg) {
// We didn't find any errors but there's probably a parse notice.
$this->errorfiles['PHP' . $filename] = 'PHP parser errors -' . $php_errormsg;
}
$this->setDebug($debug);
return \count($errors);
}
/**
* Get a metadata language property.
*
* @param string $property The name of the property.
* @param mixed $default The default value.
*
* @return mixed The value of the property.
*
* @since 1.0
*/
public function get($property, $default = null)
{
return $this->metadata[$property] ?? $default;
}
/**
* Get the base path for the instance.
*
* @return string
*
* @since 2.0.0-alpha
*/
public function getBasePath(): string
{
return $this->basePath;
}
/**
* Determine who called Language or Text.
*
* @return mixed Caller information or null if unavailable
*
* @since 1.0
*/
protected function getCallerInfo()
{
// Try to determine the source if none was provided
if (!\function_exists('debug_backtrace')) {
return;
}
$backtrace = debug_backtrace();
$info = [];
// Search through the backtrace to our caller
$continue = true;
while ($continue && next($backtrace)) {
$step = current($backtrace);
$class = @ $step['class'];
// We're looking for something outside of language.php
if ($class != __CLASS__ && $class != Text::class) {
$info['function'] = @ $step['function'];
$info['class'] = $class;
$info['step'] = prev($backtrace);
// Determine the file and name of the file
$info['file'] = @ $step['file'];
$info['line'] = @ $step['line'];
$continue = false;
}
}
return $info;
}
/**
* Getter for Name.
*
* @return string Official name element of the language.
*
* @since 1.0
*/
public function getName()
{
return $this->metadata['name'];
}
/**
* Get a list of language files that have been loaded.
*
* @param string $extension An optional extension name.
*
* @return array
*
* @since 1.0
*/
public function getPaths($extension = null)
{
if (isset($extension)) {
return $this->paths[$extension] ?? null;
}
return $this->paths;
}
/**
* Get a list of language files that are in error state.
*
* @return array
*
* @since 1.0
*/
public function getErrorFiles()
{
return $this->errorfiles;
}
/**
* Getter for the language tag (as defined in RFC 3066)
*
* @return string The language tag.
*
* @since 1.0
*/
public function getTag()
{
return $this->metadata['tag'];
}
/**
* Get the RTL property.
*
* @return boolean True is it an RTL language.
*
* @since 1.0
*/
public function isRtl()
{
return (bool) $this->metadata['rtl'];
}
/**
* Set the Debug property.
*
* @param boolean $debug The debug setting.
*
* @return boolean Previous value.
*
* @since 1.0
*/
public function setDebug($debug)
{
$previous = $this->debug;
$this->debug = (bool) $debug;
return $previous;
}
/**
* Get the Debug property.
*
* @return boolean True is in debug mode.
*
* @since 1.0
*/
public function getDebug()
{
return $this->debug;
}
/**
* Get the default language code.
*
* @return string Language code.
*
* @since 1.0
*/
public function getDefault()
{
return $this->default;
}
/**
* Set the default language code.
*
* @param string $lang The language code.
*
* @return string Previous value.
*
* @since 1.0
*/
public function setDefault($lang)
{
$previous = $this->default;
$this->default = $lang;
return $previous;
}
/**
* Get the list of orphaned strings if being tracked.
*
* @return array Orphaned text.
*
* @since 1.0
*/
public function getOrphans()
{
return $this->orphans;
}
/**
* Get the list of used strings.
*
* Used strings are those strings requested and found either as a string or a constant.
*
* @return array Used strings.
*
* @since 1.0
*/
public function getUsed()
{
return $this->used;
}
/**
* Determines is a key exists.
*
* @param string $string The key to check.
*
* @return boolean True, if the key exists.
*
* @since 1.0
*/
public function hasKey($string)
{
return $this->catalogue->hasMessage($string);
}
/**
* Returns a associative array holding the metadata.
*
* @param string $lang The name of the language.
* @param string $basePath The filepath to the language folder.
*
* @return mixed If $lang exists return key/value pair with the language metadata, otherwise return NULL.
*
* @see LanguageHelper::getMetadata()
* @since 1.0
* @deprecated 3.0 Use LanguageHelper::getMetadata() instead
*/
public static function getMetadata($lang, $basePath)
{
trigger_deprecation(
'joomla/language',
'2.0.0',
'%s() is deprecated and will be removed in 3.0, use %s::getMetadata() instead.',
__METHOD__,
LanguageHelper::class
);
return (new LanguageHelper())->getMetadata($lang, $basePath);
}
/**
* Returns a list of known languages for an area
*
* @param string $basePath The basepath to use
*
* @return array key/value pair with the language file and real name.
*
* @see LanguageHelper::getKnownLanguages()
* @since 1.0
* @deprecated 3.0 Use LanguageHelper::getKnownLanguages() instead
*/
public static function getKnownLanguages($basePath = '')
{
trigger_deprecation(
'joomla/language',
'2.0.0',
'%s() is deprecated and will be removed in 3.0, use %s::getKnownLanguages() instead.',
__METHOD__,
LanguageHelper::class
);
return (new LanguageHelper())->getKnownLanguages($basePath);
}
/**
* Get the path to a language
*
* @param string $basePath The basepath to use.
* @param string $language The language tag.
*
* @return string language related path or null.
*
* @see LanguageHelper::getLanguagePath()
* @since 1.0
* @deprecated 3.0 Use LanguageHelper::getLanguagePath() instead
*/
public static function getLanguagePath($basePath = '', $language = '')
{
trigger_deprecation(
'joomla/language',
'2.0.0',
'%s() is deprecated and will be removed in 3.0, use %s::getLanguagePath() instead.',
__METHOD__,
LanguageHelper::class
);
return (new LanguageHelper())->getLanguagePath($basePath, $language);
}
/**
* Get the current language code.
*
* @return string The language code
*
* @since 1.0
*/
public function getLanguage()
{
return $this->lang;
}
/**
* Get the message catalogue for the language.
*
* @return MessageCatalogue
*
* @since 2.0.0-alpha
*/
public function getCatalogue(): MessageCatalogue
{
return $this->catalogue;
}
/**
* Set the message catalogue for the language.
*
* @param MessageCatalogue $catalogue The message catalogue to use.
*
* @return void
*
* @since 2.0.0-alpha
*/
public function setCatalogue(MessageCatalogue $catalogue): void
{
$this->catalogue = $catalogue;
}
/**
* Get the language locale based on current language.
*
* @return array The locale according to the language.
*
* @since 1.0
*/
public function getLocale()
{
if (!isset($this->locale)) {
$locale = str_replace(' ', '', $this->metadata['locale'] ?? '');
$this->locale = $locale ? explode(',', $locale) : false;
}
return $this->locale;
}
/**
* Get the first day of the week for this language.
*
* @return integer The first day of the week according to the language
*
* @since 1.0
*/
public function getFirstDay()
{
return (int) ($this->metadata['firstDay'] ?? 0);
}
/**
* Get the weekends days for this language.
*
* @return string The weekend days of the week separated by a comma according to the language
*
* @since 2.0.0-alpha
*/
public function getWeekEnd(): string
{
return $this->metadata['weekEnd'] ?? '0,6';
}
/**
* Searches for language directories within a certain base dir.
*
* @param string $dir directory of files.
*
* @return array Array holding the found languages as filename => real name pairs.
*
* @see LanguageHelper::parseLanguageFiles()
* @since 1.0
* @deprecated 3.0 Use LanguageHelper::parseLanguageFiles() instead
*/
public static function parseLanguageFiles($dir = null)
{
trigger_deprecation(
'joomla/language',
'2.0.0',
'%s() is deprecated and will be removed in 3.0, use %s::parseLanguageFiles() instead.',
__METHOD__,
LanguageHelper::class
);
return (new LanguageHelper())->parseLanguageFiles($dir);
}
/**
* Parse XML file for language information.
*
* @param string $path Path to the XML files.
*
* @return mixed Array holding the found metadata as a key => value pair or null on an invalid XML file
*
* @see LanguageHelper::parseXMLLanguageFile()
* @since 1.0
* @deprecated 3.0 Use LanguageHelper::parseXMLLanguageFile() instead
*/
public static function parseXmlLanguageFile($path)
{
trigger_deprecation(
'joomla/language',
'2.0.0',
'%s() is deprecated and will be removed in 3.0, use %s::parseXmlLanguageFile() instead.',
__METHOD__,
LanguageHelper::class
);
return (new LanguageHelper())->parseXMLLanguageFile($path);
}
}

View File

@ -0,0 +1,217 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language;
/**
* Language package factory
*
* @since 1.3.0
*/
class LanguageFactory
{
/**
* Application's default language
*
* @var string
* @since 1.3.0
*/
private $defaultLanguage = 'en-GB';
/**
* Path to the directory containing the application's language folder
*
* @var string
* @since 2.0.0-alpha
*/
private $languageDirectory = '';
/**
* Get the application's default language.
*
* @return string
*
* @since 1.3.0
*/
public function getDefaultLanguage()
{
return $this->defaultLanguage;
}
/**
* Creates a new Language instance based on the given parameters.
*
* @param string $lang The language to use.
* @param string $path The base path to the language folder. This is required if creating a new instance.
* @param boolean $debug The debug mode.
*
* @return Language
*
* @since 1.3.0
*/
public function getLanguage($lang = '', $path = '', $debug = false)
{
$path = $path ?: $this->getLanguageDirectory();
$lang = $lang ?: $this->getDefaultLanguage();
$loaderRegistry = new ParserRegistry();
$loaderRegistry->add(new Parser\IniParser());
return new Language($loaderRegistry, $path, $lang, $debug);
}
/**
* Get the path to the directory containing the application's language folder.
*
* @return string
*
* @since 1.3.0
*/
public function getLanguageDirectory()
{
return $this->languageDirectory;
}
/**
* Creates a new LocaliseInterface instance for the language.
*
* @param string $lang Language to check.
* @param string $basePath Base path to the language folder.
*
* @return LocaliseInterface
*
* @since 2.0.0-alpha
*/
public function getLocalise(string $lang, string $basePath = ''): LocaliseInterface
{
/*
* Look for a language specific localise class
*
* LocaliseInterface classes are searched for in the global namespace and are named based
* on the language code, replacing hyphens with underscores (i.e. en-GB looks for En_GBLocalise)
*/
$class = str_replace('-', '_', $lang . 'Localise');
// If this class already exists, no need to try and find it
if (class_exists($class)) {
return new $class();
}
$paths = [];
$basePath = $basePath ?: $this->getLanguageDirectory();
// Get the LanguageHelper to set the proper language directory
$basePath = (new LanguageHelper())->getLanguagePath($basePath);
// Explicitly set the keys to define the lookup order
$paths[0] = $basePath . "/overrides/$lang.localise.php";
$paths[1] = $basePath . "/$lang/$lang.localise.php";
ksort($paths);
$path = reset($paths);
while (!class_exists($class) && $path) {
if (file_exists($path)) {
require_once $path;
}
$path = next($paths);
}
// If we have found a match initialise it and return it
if (class_exists($class)) {
return new $class();
}
// Return the en_GB class if no specific instance is found
return new Localise\En_GBLocalise();
}
/**
* Creates a new StemmerInterface instance for the requested adapter.
*
* @param string $adapter The type of stemmer to load.
*
* @return StemmerInterface
*
* @since 1.3.0
* @throws \RuntimeException on invalid stemmer
*/
public function getStemmer($adapter)
{
// Setup the adapter for the stemmer.
$class = __NAMESPACE__ . '\\Stemmer\\' . ucfirst(trim($adapter));
// Check if a stemmer exists for the adapter.
if (!class_exists($class)) {
// Throw invalid adapter exception.
throw new \RuntimeException(sprintf('Invalid stemmer type %s', $class));
}
return new $class();
}
/**
* Retrieves a new Text object for a Language instance
*
* @param Language|null $language An optional Language object to inject, otherwise the default object is loaded
*
* @return Text
*
* @since 2.0.0-alpha
*/
public function getText(?Language $language = null): Text
{
$language = $language ?: $this->getLanguage();
return new Text($language);
}
/**
* Set the application's default language
*
* @param string $language Language code for the application's default language
*
* @return $this
*
* @since 1.3.0
*/
public function setDefaultLanguage($language)
{
$this->defaultLanguage = $language;
return $this;
}
/**
* Set the path to the directory containing the application's language folder
*
* @param string $directory Path to the application's language folder
*
* @return $this
*
* @since 2.0.0-alpha
*/
public function setLanguageDirectory(string $directory): self
{
if (!is_dir($directory)) {
throw new \InvalidArgumentException(
sprintf(
'Cannot set language directory to "%s" since the directory does not exist.',
$directory
)
);
}
$this->languageDirectory = $directory;
return $this;
}
}

View File

@ -0,0 +1,173 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language;
/**
* Helper class for the Language package
*
* @since 2.0.0-alpha
*/
class LanguageHelper
{
/**
* Checks if a language exists.
*
* This is a simple, quick check for the directory that should contain language files for the given user.
*
* @param string $lang Language to check.
* @param string $basePath Directory to check for the specified language.
*
* @return boolean True if the language exists.
*
* @since 2.0.0-alpha
*/
public function exists(string $lang, string $basePath): bool
{
return is_dir($this->getLanguagePath($basePath, $lang));
}
/**
* Returns a associative array holding the metadata.
*
* @param string $lang The name of the language.
* @param string $path The filepath to the language folder.
*
* @return array|null If $lang exists return key/value pair with the language metadata, otherwise return NULL.
*
* @since 2.0.0-alpha
*/
public function getMetadata(string $lang, string $path): ?array
{
$path = $this->getLanguagePath($path, $lang);
$file = $lang . '.xml';
$result = null;
if (is_file("$path/$file")) {
$result = $this->parseXMLLanguageFile("$path/$file");
}
if (empty($result)) {
return null;
}
return $result;
}
/**
* Returns a list of known languages for an area
*
* @param string $basePath The basepath to use
*
* @return array key/value pair with the language file and real name.
*
* @since 2.0.0-alpha
*/
public function getKnownLanguages(string $basePath): array
{
return $this->parseLanguageFiles($this->getLanguagePath($basePath));
}
/**
* Get the path to a language
*
* @param string $basePath The basepath to use.
* @param string $language The language tag.
*
* @return string Path to the language folder
*
* @since 2.0.0-alpha
*/
public function getLanguagePath(string $basePath, string $language = ''): string
{
$dir = $basePath . '/language';
if (!empty($language)) {
$dir .= '/' . $language;
}
return $dir;
}
/**
* Searches for language directories within a certain base dir.
*
* @param string $dir directory of files.
*
* @return array Array holding the found languages as filename => real name pairs.
*
* @since 2.0.0-alpha
*/
public function parseLanguageFiles(string $dir = ''): array
{
$languages = [];
// Search main language directory for subdirectories
foreach (glob($dir . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $directory) {
// But only directories with lang code format
if (preg_match('#/[a-z]{2,3}-[A-Z]{2}$#', $directory)) {
$dirPathParts = pathinfo($directory);
$file = $directory . '/' . $dirPathParts['filename'] . '.xml';
if (!is_file($file)) {
continue;
}
try {
// Get installed language metadata from xml file and merge it with lang array
if ($metadata = $this->parseXMLLanguageFile($file)) {
$languages = array_replace($languages, [$dirPathParts['filename'] => $metadata]);
}
} catch (\RuntimeException $e) {
}
}
}
return $languages;
}
/**
* Parse XML file for language information.
*
* @param string $path Path to the XML files.
*
* @return array|null Array holding the found metadata as a key => value pair.
*
* @since 2.0.0-alpha
* @throws \RuntimeException
*/
public function parseXmlLanguageFile(string $path): ?array
{
if (!is_readable($path)) {
throw new \RuntimeException('File not found or not readable');
}
// Try to load the file
$xml = simplexml_load_file($path);
if (!$xml) {
return null;
}
// Check that it's a metadata file
if ((string) $xml->getName() != 'metafile') {
return null;
}
$metadata = [];
/** @var \SimpleXMLElement $child */
foreach ($xml->metadata->children() as $child) {
$metadata[$child->getName()] = (string) $child;
}
return $metadata;
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language\Localise;
use Joomla\Language\LocaliseInterface;
use Joomla\Language\Transliterate;
use Joomla\String\StringHelper;
/**
* Abstract localisation handler class
*
* @since 2.0.0-alpha
*/
abstract class AbstractLocalise implements LocaliseInterface
{
/**
* Transliterate function
*
* This method processes a string and replaces all accented UTF-8 characters by unaccented ASCII-7 equivalents.
*
* @param string $string The string to transliterate.
*
* @return string|boolean The transliterated string or boolean false on a failure
*
* @since 2.0.0-alpha
*/
public function transliterate($string)
{
$string = (new Transliterate())->utf8_latin_to_ascii($string);
return StringHelper::strtolower($string);
}
/**
* Returns an array of suffixes for plural rules.
*
* @param integer $count The count number the rule is for.
*
* @return string[] The array of suffixes.
*
* @since 2.0.0-alpha
*/
public function getPluralSuffixes($count)
{
return [(string) $count];
}
}

View File

@ -0,0 +1,19 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language\Localise;
/**
* en-GB localisation handler class
*
* @since 2.0.0-alpha
*/
class En_GBLocalise extends AbstractLocalise
{
}

View File

@ -0,0 +1,43 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language;
/**
* Joomla Framework Language Interface
*
* @since 2.0.0-alpha
*/
interface LocaliseInterface
{
/**
* Transliterate function
*
* This method processes a string and replaces all accented UTF-8 characters by unaccented
* ASCII-7 "equivalents".
*
* @param string $string The string to transliterate.
*
* @return string|boolean The transliterated string or boolean false on a failure
*
* @since 2.0.0-alpha
*/
public function transliterate($string);
/**
* Returns an array of suffixes for plural rules.
*
* @param integer $count The count number the rule is for.
*
* @return string[] The array of suffixes.
*
* @since 2.0.0-alpha
*/
public function getPluralSuffixes($count);
}

View File

@ -0,0 +1,211 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language;
/**
* Catalogue of loaded translation strings for a language
*
* @since 2.0.0-alpha
*/
class MessageCatalogue
{
/**
* A fallback for this catalogue
*
* @var MessageCatalogue
* @since 2.0.0-alpha
*/
private $fallbackCatalogue;
/**
* The language of the messages in this catalogue
*
* @var string
* @since 2.0.0-alpha
*/
private $language;
/**
* The messages stored to this catalogue
*
* @var array
* @since 2.0.0-alpha
*/
private $messages = [];
/**
* MessageCatalogue constructor.
*
* @param string $language The language of the messages in this catalogue
* @param array $messages The messages to seed this catalogue with
*
* @since 2.0.0-alpha
*/
public function __construct(string $language, array $messages = [])
{
$this->language = $language;
$this->addMessages($messages);
}
/**
* Add a message to the catalogue, replacing the key if it already exists
*
* @param string $key The key identifying the message
* @param string $message The message for this key
*
* @return void
*/
public function addMessage(string $key, string $message): void
{
$this->addMessages([$key => $message]);
}
/**
* Add messages to the catalogue, replacing any keys which already exist
*
* @param array $messages An associative array containing the messages to add to the catalogue
*
* @return void
*
* @since 2.0.0-alpha
*/
public function addMessages(array $messages): void
{
$this->messages = array_replace($this->messages, array_change_key_case($messages, CASE_UPPER));
}
/**
* Check if this catalogue has a message for the given key, ignoring a fallback if defined
*
* @param string $key The key to check
*
* @return boolean
*
* @since 2.0.0-alpha
*/
public function definesMessage(string $key): bool
{
return isset($this->messages[strtoupper($key)]);
}
/**
* Get the fallback for this catalogue if set
*
* @return MessageCatalogue|null
*
* @since 2.0.0-alpha
*/
public function getFallbackCatalogue(): ?MessageCatalogue
{
return $this->fallbackCatalogue;
}
/**
* Get the language for this catalogue
*
* @return string
*
* @since 2.0.0-alpha
*/
public function getLanguage(): string
{
return $this->language;
}
/**
* Get the message for a given key
*
* @param string $key The key to get the message for
*
* @return string The message if one is set otherwise the key
*
* @since 2.0.0-alpha
*/
public function getMessage(string $key): string
{
if ($this->definesMessage($key)) {
return $this->messages[strtoupper($key)];
}
if ($this->fallbackCatalogue) {
return $this->fallbackCatalogue->getMessage($key);
}
return strtoupper($key);
}
/**
* Fetch the messages stored in this catalogue
*
* @return array
*
* @since 2.0.0-alpha
*/
public function getMessages(): array
{
return $this->messages;
}
/**
* Check if the catalogue has a message for the given key
*
* @param string $key The key to check
*
* @return boolean
*
* @since 2.0.0-alpha
*/
public function hasMessage(string $key): bool
{
if ($this->definesMessage($key)) {
return true;
}
if ($this->fallbackCatalogue) {
return $this->fallbackCatalogue->hasMessage($key);
}
return false;
}
/**
* Merge another catalogue into this one
*
* @param MessageCatalogue $messageCatalogue The catalogue to merge
*
* @return void
*
* @since 2.0.0-alpha
* @throws \LogicException
*/
public function mergeCatalogue(MessageCatalogue $messageCatalogue): void
{
if ($messageCatalogue->getLanguage() !== $this->getLanguage()) {
throw new \LogicException('Cannot merge a catalogue that does not have the same language code.');
}
$this->addMessages($messageCatalogue->getMessages());
}
/**
* Set the fallback for this catalogue
*
* @param MessageCatalogue $messageCatalogue The catalogue to use as the fallback
*
* @return void
*
* @since 2.0.0-alpha
*/
public function setFallbackCatalogue(MessageCatalogue $messageCatalogue): void
{
$this->fallbackCatalogue = $messageCatalogue;
}
}

View File

@ -0,0 +1,129 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language\Parser;
use Joomla\Language\DebugParserInterface;
/**
* Language file parser for INI files
*
* @since 2.0.0-alpha
*/
class IniParser implements DebugParserInterface
{
/**
* Parse a file and check its contents for valid structure
*
* @param string $filename The name of the file.
*
* @return string[] Array containing a list of errors
*
* @since 2.0.0-alpha
*/
public function debugFile(string $filename): array
{
// Initialise variables for manually parsing the file for common errors.
$blacklist = ['YES', 'NO', 'NULL', 'FALSE', 'ON', 'OFF', 'NONE', 'TRUE'];
$errors = [];
// Open the file as a stream.
foreach (new \SplFileObject($filename) as $lineNumber => $line) {
// Avoid BOM error as BOM is OK when using parse_ini.
if ($lineNumber == 0) {
$line = str_replace("\xEF\xBB\xBF", '', $line);
}
$line = trim($line);
// Ignore comment lines.
if (!\strlen($line) || $line[0] == ';') {
continue;
}
// Ignore grouping tag lines, like: [group]
if (preg_match('#^\[[^\]]*\](\s*;.*)?$#', $line)) {
continue;
}
$realNumber = $lineNumber + 1;
// Check for any incorrect uses of _QQ_.
if (strpos($line, '_QQ_') !== false) {
$errors[] = 'The deprecated constant `_QQ_` is in use on line ' . $realNumber;
continue;
}
// Check for odd number of double quotes.
if (substr_count($line, '"') % 2 != 0) {
$errors[] = 'There are an odd number of quotes on line ' . $realNumber;
continue;
}
// Check that the line passes the necessary format.
if (!preg_match('#^[A-Z][A-Z0-9_\*\-\.]*\s*=\s*".*"(\s*;.*)?$#', $line)) {
$errors[] = 'The language key does not meet the required format on line ' . $realNumber;
continue;
}
// Check that the key is not in the blacklist.
$key = strtoupper(trim(substr($line, 0, strpos($line, '='))));
if (\in_array($key, $blacklist)) {
$errors[] = 'The language key "' . $key . '" is a blacklisted key on line ' . $realNumber;
$errors[] = $realNumber;
}
}
return $errors;
}
/**
* Get the type of parser
*
* @return string
*
* @since 2.0.0-alpha
*/
public function getType(): string
{
return 'ini';
}
/**
* Load the strings from a file
*
* @param string $filename The name of the file.
*
* @return string[]
*
* @since 2.0.0-alpha
* @throws \RuntimeException on a load/parse error
*/
public function loadFile(string $filename): array
{
$result = @parse_ini_file($filename);
if ($result === false) {
$lastError = error_get_last();
$errorMessage = $lastError['message'] ?? 'Unknown Error';
throw new \RuntimeException(
sprintf('Could not process file `%s`: %s', $errorMessage, $filename)
);
}
return $result;
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language;
/**
* Interface describing a language file loader
*
* @since 2.0.0-alpha
*/
interface ParserInterface
{
/**
* Get the type of loader
*
* @return string
*
* @since 2.0.0-alpha
*/
public function getType(): string;
/**
* Load the strings from a file
*
* @param string $filename The name of the file.
*
* @return string[]
*
* @since 2.0.0-alpha
* @throws \RuntimeException on a load/parse error
*/
public function loadFile(string $filename): array;
}

View File

@ -0,0 +1,72 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language;
/**
* Registry of file parsers
*
* @since 2.0.0-alpha
*/
class ParserRegistry
{
/**
* A map of the registered parsers
*
* @var ParserInterface[]
* @since 2.0.0-alpha
*/
private $parserMap = [];
/**
* Register a parser, overridding a previously registered parser for the given type
*
* @param ParserInterface $parser The parser to registery
*
* @return void
*
* @since 2.0.0-alpha
*/
public function add(ParserInterface $parser): void
{
$this->parserMap[$parser->getType()] = $parser;
}
/**
* Get the parser for a given type
*
* @param string $type The parser type to retrieve
*
* @return ParserInterface
*
* @since 2.0.0-alpha
*/
public function get(string $type): ParserInterface
{
if (!$this->has($type)) {
throw new \InvalidArgumentException(sprintf('There is not a parser registered for the `%s` type.', $type));
}
return $this->parserMap[$type];
}
/**
* Check if a parser is registered for the given type
*
* @param string $type The parser type to check (typically the file extension)
*
* @return boolean
*
* @since 2.0.0-alpha
*/
public function has(string $type): bool
{
return isset($this->parserMap[$type]);
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language\Service;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
use Joomla\Language\LanguageFactory;
/**
* LanguageFactory object service provider
*
* @since 2.0.0-alpha
*/
class LanguageFactoryProvider implements ServiceProviderInterface
{
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 2.0.0-alpha
* @throws \RuntimeException
*/
public function register(Container $container)
{
$container->share(
'Joomla\\Language\\LanguageFactory',
function (Container $container) {
$factory = new LanguageFactory();
/** @var \Joomla\Registry\Registry $config */
$config = $container->get('config');
$baseLangDir = $config->get('language.basedir');
$defaultLang = $config->get('language.default', 'en-GB');
if ($baseLangDir) {
$factory->setLanguageDirectory($baseLangDir);
}
$factory->setDefaultLanguage($defaultLang);
return $factory;
},
true
);
}
}

View File

@ -0,0 +1,484 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @copyright Copyright (C) 2005 Richard Heyes (http://www.phpguru.org/). All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language\Stemmer;
use Joomla\Language\StemmerInterface;
/**
* Porter English stemmer class.
*
* This class was adapted from one written by Richard Heyes.
* See copyright and link information above.
*
* @since 1.0
*/
class Porteren implements StemmerInterface
{
/**
* An internal cache of stemmed tokens.
*
* @var array
* @since 1.0
*/
protected $cache = [];
/**
* Regex for matching a consonant.
*
* @var string
* @since 1.4.0
*/
private $regexConsonant = '(?:[bcdfghjklmnpqrstvwxz]|(?<=[aeiou])y|^y)';
/**
* Regex for matching a vowel
*
* @var string
* @since 1.4.0
*/
private $regexVowel = '(?:[aeiou]|(?<![aeiou])y)';
/**
* Method to stem a token and return the root.
*
* @param string $token The token to stem.
* @param string $lang The language of the token.
*
* @return string The root token.
*
* @since 1.0
*/
public function stem($token, $lang)
{
// Check if the token is long enough to merit stemming.
if (\strlen($token) <= 2) {
return $token;
}
// Check if the language is English or All.
if ($lang !== 'en') {
return $token;
}
// Stem the token if it is not in the cache.
if (!isset($this->cache[$lang][$token])) {
// Stem the token.
$result = $token;
$result = $this->step1ab($result);
$result = $this->step1c($result);
$result = $this->step2($result);
$result = $this->step3($result);
$result = $this->step4($result);
$result = $this->step5($result);
// Add the token to the cache.
$this->cache[$lang][$token] = $result;
}
return $this->cache[$lang][$token];
}
/**
* Step 1
*
* @param string $word The token to stem.
*
* @return string
*
* @since 1.0
*/
private function step1ab($word)
{
// Part a
if (substr($word, -1) == 's') {
$this->replace($word, 'sses', 'ss')
|| $this->replace($word, 'ies', 'i')
|| $this->replace($word, 'ss', 'ss')
|| $this->replace($word, 's', '');
}
// Part b
if (substr($word, -2, 1) != 'e' || !$this->replace($word, 'eed', 'ee', 0)) {
// First rule
$v = $this->regexVowel;
// Check ing and ed
// Note use of && and OR, for precedence reasons
if (
preg_match("#$v+#", substr($word, 0, -3)) && $this->replace($word, 'ing', '')
|| preg_match("#$v+#", substr($word, 0, -2)) && $this->replace($word, 'ed', '')
) {
// If one of above two test successful
if (!$this->replace($word, 'at', 'ate') && !$this->replace($word, 'bl', 'ble') && !$this->replace($word, 'iz', 'ize')) {
// Double consonant ending
if ($this->doubleConsonant($word) && substr($word, -2) != 'll' && substr($word, -2) != 'ss' && substr($word, -2) != 'zz') {
$word = substr($word, 0, -1);
} elseif ($this->m($word) == 1 && $this->cvc($word)) {
$word .= 'e';
}
}
}
}
return $word;
}
/**
* Step 1c
*
* @param string $word The token to stem.
*
* @return string
*
* @since 1.0
*/
private function step1c($word)
{
$v = $this->regexVowel;
if (substr($word, -1) == 'y' && preg_match("#$v+#", substr($word, 0, -1))) {
$this->replace($word, 'y', 'i');
}
return $word;
}
/**
* Step 2
*
* @param string $word The token to stem.
*
* @return string
*
* @since 1.0
*/
private function step2($word)
{
switch (substr($word, -2, 1)) {
case 'a':
$this->replace($word, 'ational', 'ate', 0)
|| $this->replace($word, 'tional', 'tion', 0);
break;
case 'c':
$this->replace($word, 'enci', 'ence', 0)
|| $this->replace($word, 'anci', 'ance', 0);
break;
case 'e':
$this->replace($word, 'izer', 'ize', 0);
break;
case 'g':
$this->replace($word, 'logi', 'log', 0);
break;
case 'l':
$this->replace($word, 'entli', 'ent', 0)
|| $this->replace($word, 'ousli', 'ous', 0)
|| $this->replace($word, 'alli', 'al', 0)
|| $this->replace($word, 'bli', 'ble', 0)
|| $this->replace($word, 'eli', 'e', 0);
break;
case 'o':
$this->replace($word, 'ization', 'ize', 0)
|| $this->replace($word, 'ation', 'ate', 0)
|| $this->replace($word, 'ator', 'ate', 0);
break;
case 's':
$this->replace($word, 'iveness', 'ive', 0)
|| $this->replace($word, 'fulness', 'ful', 0)
|| $this->replace($word, 'ousness', 'ous', 0)
|| $this->replace($word, 'alism', 'al', 0);
break;
case 't':
$this->replace($word, 'biliti', 'ble', 0)
|| $this->replace($word, 'aliti', 'al', 0)
|| $this->replace($word, 'iviti', 'ive', 0);
break;
}
return $word;
}
/**
* Step 3
*
* @param string $word The token to stem.
*
* @return string
*
* @since 1.0
*/
private function step3($word)
{
switch (substr($word, -2, 1)) {
case 'a':
$this->replace($word, 'ical', 'ic', 0);
break;
case 's':
$this->replace($word, 'ness', '', 0);
break;
case 't':
$this->replace($word, 'icate', 'ic', 0)
|| $this->replace($word, 'iciti', 'ic', 0);
break;
case 'u':
$this->replace($word, 'ful', '', 0);
break;
case 'v':
$this->replace($word, 'ative', '', 0);
break;
case 'z':
$this->replace($word, 'alize', 'al', 0);
break;
}
return $word;
}
/**
* Step 4
*
* @param string $word The token to stem.
*
* @return string
*
* @since 1.0
*/
private function step4($word)
{
switch (substr($word, -2, 1)) {
case 'a':
$this->replace($word, 'al', '', 1);
break;
case 'c':
$this->replace($word, 'ance', '', 1)
|| $this->replace($word, 'ence', '', 1);
break;
case 'e':
$this->replace($word, 'er', '', 1);
break;
case 'i':
$this->replace($word, 'ic', '', 1);
break;
case 'l':
$this->replace($word, 'able', '', 1)
|| $this->replace($word, 'ible', '', 1);
break;
case 'n':
$this->replace($word, 'ant', '', 1)
|| $this->replace($word, 'ement', '', 1)
|| $this->replace($word, 'ment', '', 1)
|| $this->replace($word, 'ent', '', 1);
break;
case 'o':
if (substr($word, -4) == 'tion' || substr($word, -4) == 'sion') {
$this->replace($word, 'ion', '', 1);
} else {
$this->replace($word, 'ou', '', 1);
}
break;
case 's':
$this->replace($word, 'ism', '', 1);
break;
case 't':
$this->replace($word, 'ate', '', 1)
|| $this->replace($word, 'iti', '', 1);
break;
case 'u':
$this->replace($word, 'ous', '', 1);
break;
case 'v':
$this->replace($word, 'ive', '', 1);
break;
case 'z':
$this->replace($word, 'ize', '', 1);
break;
}
return $word;
}
/**
* Step 5
*
* @param string $word The token to stem.
*
* @return string
*
* @since 1.0
*/
private function step5($word)
{
// Part a
if (substr($word, -1) == 'e') {
if ($this->m(substr($word, 0, -1)) > 1) {
$this->replace($word, 'e', '');
} elseif ($this->m(substr($word, 0, -1)) == 1) {
if (!$this->cvc(substr($word, 0, -1))) {
$this->replace($word, 'e', '');
}
}
}
// Part b
if ($this->m($word) > 1 && $this->doubleConsonant($word) && substr($word, -1) == 'l') {
$word = substr($word, 0, -1);
}
return $word;
}
/**
* Replaces the first string with the second, at the end of the string. If third
* arg is given, then the preceding string must match that m count at least.
*
* @param string $str String to check
* @param string $check Ending to check for
* @param string $repl Replacement string
* @param integer $m Optional minimum number of m() to meet
*
* @return boolean Whether the $check string was at the end
* of the $str string. True does not necessarily mean
* that it was replaced.
*
* @since 1.0
*/
private function replace(&$str, $check, $repl, $m = null)
{
$len = 0 - \strlen($check);
if (substr($str, $len) == $check) {
$substr = substr($str, 0, $len);
if ($m === null || $this->m($substr) > $m) {
$str = $substr . $repl;
}
return true;
}
return false;
}
/**
* m() measures the number of consonant sequences in $str. if c is
* a consonant sequence and v a vowel sequence, and <..> indicates arbitrary
* presence,
*
* <c><v> gives 0
* <c>vc<v> gives 1
* <c>vcvc<v> gives 2
* <c>vcvcvc<v> gives 3
*
* @param string $str The string to return the m count for
*
* @return integer The m count
*
* @since 1.0
*/
private function m($str)
{
$c = $this->regexConsonant;
$v = $this->regexVowel;
$str = preg_replace("#^$c+#", '', $str);
$str = preg_replace("#$v+$#", '', $str);
preg_match_all("#($v+$c+)#", $str, $matches);
return \count($matches[1]);
}
/**
* Returns true/false as to whether the given string contains two
* of the same consonant next to each other at the end of the string.
*
* @param string $str String to check
*
* @return boolean Result
*
* @since 1.0
*/
private function doubleConsonant($str)
{
$c = $this->regexConsonant;
return preg_match("#$c{2}$#", $str, $matches) && $matches[0][0] === $matches[0][1];
}
/**
* Checks for ending CVC sequence where second C is not W, X or Y
*
* @param string $str String to check
*
* @return boolean Result
*
* @since 1.0
*/
private function cvc($str)
{
$c = $this->regexConsonant;
$v = $this->regexVowel;
return preg_match("#($c$v$c)$#", $str, $matches)
&& \strlen($matches[1]) === 3
&& $matches[1][2] !== 'w'
&& $matches[1][2] !== 'x'
&& $matches[1][2] !== 'y';
}
}

View File

@ -0,0 +1,30 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language;
/**
* Stemmer interface.
*
* @since 1.4.0
*/
interface StemmerInterface
{
/**
* Method to stem a token and return the root.
*
* @param string $token The token to stem.
* @param string $lang The language of the token.
*
* @return string The root token.
*
* @since 1.4.0
*/
public function stem($token, $lang);
}

View File

@ -0,0 +1,255 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language;
/**
* Text handling class.
*
* @since 1.0
*/
class Text
{
/**
* Language instance
*
* @var Language
* @since 1.0
*/
private $language;
/**
* Constructor
*
* @param Language $language Language instance to use in translations
*
* @since 2.0.0-alpha
*/
public function __construct(Language $language)
{
$this->setLanguage($language);
}
/**
* Retrieve the current Language instance
*
* @return Language
*
* @since 2.0.0-alpha
*/
public function getLanguage(): Language
{
return $this->language;
}
/**
* Set the Language object
*
* @param Language $language Language instance
*
* @return $this
*
* @since 2.0.0-alpha
*/
public function setLanguage(Language $language): self
{
$this->language = $language;
return $this;
}
/**
* Translates a string into the current language.
*
* @param string $string The string to translate.
* @param array $parameters Array of parameters for the string
* @param boolean $jsSafe True to escape the string for JavaScript output
* @param boolean $interpretBackSlashes To interpret backslashes (\\=\, \n=carriage return, \t=tabulation)
*
* @return string The translated string or the key if $script is true
*
* @since 2.0.0-alpha
*/
public function translate(string $string, array $parameters = [], bool $jsSafe = false, bool $interpretBackSlashes = true): string
{
$translated = $this->getLanguage()->translate($string, $jsSafe, $interpretBackSlashes);
if (!empty($parameters)) {
$translated = strtr($translated, $parameters);
}
return $translated;
}
/**
* Translates a string into the current language.
*
* @param string $string The string to translate.
* @param string $alt The alternate option for global string
* @param array $parameters Array of parameters for the string
* @param mixed $jsSafe Boolean: Make the result javascript safe.
* @param boolean $interpretBackSlashes To interpret backslashes (\\=\, \n=carriage return, \t=tabulation)
*
* @return string The translated string or the key if $script is true
*
* @since 1.0
*/
public function alt($string, $alt, array $parameters = [], $jsSafe = false, $interpretBackSlashes = true)
{
if ($this->getLanguage()->hasKey($string . '_' . $alt)) {
return $this->translate($string . '_' . $alt, $parameters, $jsSafe, $interpretBackSlashes);
}
return $this->translate($string, $parameters, $jsSafe, $interpretBackSlashes);
}
/**
* Pluralises a string in the current language
*
* The last argument can take an array of options to configure the call to `Joomla\Language\Language::translate()`:
*
* array(
* 'jsSafe' => boolean,
* 'interpretBackSlashes' =>boolean
* )
*
* where:
*
* jsSafe is a boolean to specify whether to make the result JavaScript safe.
* interpretBackSlashes is a boolean to specify whether backslashes are interpreted (\\ -> \, \n -> new line, \t -> tab character).
*
* @param string $string The format string.
* @param integer $n The number of items
*
* @return string The translated string
*
* @note This method can take a mixed number of arguments for the sprintf function
* @since 1.0
*/
public function plural($string, $n)
{
$lang = $this->getLanguage();
$args = \func_get_args();
$count = \count($args);
// Try the key from the language plural potential suffixes
$found = false;
$suffixes = $lang->getPluralSuffixes((int) $n);
array_unshift($suffixes, (int) $n);
foreach ($suffixes as $suffix) {
$key = $string . '_' . $suffix;
if ($lang->hasKey($key)) {
$found = true;
break;
}
}
if (!$found) {
// Not found so revert to the original.
$key = $string;
}
if (\is_array($args[$count - 1])) {
$args[0] = $lang->translate(
$key,
$args[$count - 1]['jsSafe'] ?? false,
$args[$count - 1]['interpretBackSlashes'] ?? true
);
} else {
$args[0] = $lang->translate($key);
}
return \sprintf(...$args);
}
/**
* Passes a string thru a sprintf.
*
* The last argument can take an array of options to configure the call to `Joomla\Language\Language::translate()`:
*
* array(
* 'jsSafe' => boolean,
* 'interpretBackSlashes' =>boolean
* )
*
* where:
*
* jsSafe is a boolean to specify whether to make the result JavaScript safe.
* interpretBackSlashes is a boolean to specify whether backslashes are interpreted (\\ -> \, \n -> new line, \t -> tab character).
*
* @param string $string The format string.
*
* @return string|null The translated string
*
* @note This method can take a mixed number of arguments for the sprintf function
* @since 1.0
*/
public function sprintf($string)
{
$lang = $this->getLanguage();
$args = \func_get_args();
$count = \count($args);
if (\is_array($args[$count - 1])) {
$args[0] = $lang->translate(
$string,
$args[$count - 1]['jsSafe'] ?? false,
$args[$count - 1]['interpretBackSlashes'] ?? true
);
} else {
$args[0] = $lang->translate($string);
}
return \sprintf(...$args);
}
/**
* Passes a string thru an printf.
*
* The last argument can take an array of options to configure the call to `Joomla\Language\Language::translate()`:
*
* array(
* 'jsSafe' => boolean,
* 'interpretBackSlashes' =>boolean
* )
*
* where:
*
* jsSafe is a boolean to specify whether to make the result JavaScript safe.
* interpretBackSlashes is a boolean to specify whether backslashes are interpreted (\\ -> \, \n -> new line, \t -> tab character).
*
* @param string $string The format string.
*
* @return string|null The translated string
*
* @note This method can take a mixed number of arguments for the printf function
* @since 1.0
*/
public function printf($string)
{
$lang = $this->getLanguage();
$args = \func_get_args();
$count = \count($args);
if (\is_array($args[$count - 1])) {
$args[0] = $lang->translate(
$string,
$args[$count - 1]['jsSafe'] ?? false,
$args[$count - 1]['interpretBackSlashes'] ?? true
);
} else {
$args[0] = $lang->translate($string);
}
return \printf(...$args);
}
}

View File

@ -0,0 +1,268 @@
<?php
/**
* Part of the Joomla Framework Language Package
*
* @copyright Copyright (C) 2005 - 2020 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Language;
/**
* Class to transliterate strings
*
* @since 1.0
* @note Port of phputf8's utf8_accents_to_ascii()
*/
class Transliterate
{
/**
* Map of lowercased UTF-8 characters with their latin equivalents
*
* @var array
* @since 1.4.0
*/
private static $utf8LowerAccents = [
'à' => 'a',
'ô' => 'o',
'ď' => 'd',
'ḟ' => 'f',
'ë' => 'e',
'š' => 's',
'ơ' => 'o',
'ß' => 'ss',
'ă' => 'a',
'ř' => 'r',
'ț' => 't',
'ň' => 'n',
'ā' => 'a',
'ķ' => 'k',
'ŝ' => 's',
'ỳ' => 'y',
'ņ' => 'n',
'ĺ' => 'l',
'ħ' => 'h',
'ṗ' => 'p',
'ó' => 'o',
'ú' => 'u',
'ě' => 'e',
'é' => 'e',
'ç' => 'c',
'ẁ' => 'w',
'ċ' => 'c',
'õ' => 'o',
'ṡ' => 's',
'ø' => 'o',
'ģ' => 'g',
'ŧ' => 't',
'ș' => 's',
'ė' => 'e',
'ĉ' => 'c',
'ś' => 's',
'î' => 'i',
'ű' => 'u',
'ć' => 'c',
'ę' => 'e',
'ŵ' => 'w',
'ṫ' => 't',
'ū' => 'u',
'č' => 'c',
'ö' => 'oe',
'è' => 'e',
'ŷ' => 'y',
'ą' => 'a',
'ł' => 'l',
'ų' => 'u',
'ů' => 'u',
'ş' => 's',
'ğ' => 'g',
'ļ' => 'l',
'ƒ' => 'f',
'ž' => 'z',
'ẃ' => 'w',
'ḃ' => 'b',
'å' => 'a',
'ì' => 'i',
'ï' => 'i',
'ḋ' => 'd',
'ť' => 't',
'ŗ' => 'r',
'ä' => 'ae',
'í' => 'i',
'ŕ' => 'r',
'ê' => 'e',
'ü' => 'ue',
'ò' => 'o',
'ē' => 'e',
'ñ' => 'n',
'ń' => 'n',
'ĥ' => 'h',
'ĝ' => 'g',
'đ' => 'd',
'ĵ' => 'j',
'ÿ' => 'y',
'ũ' => 'u',
'ŭ' => 'u',
'ư' => 'u',
'ţ' => 't',
'ý' => 'y',
'ő' => 'o',
'â' => 'a',
'ľ' => 'l',
'ẅ' => 'w',
'ż' => 'z',
'ī' => 'i',
'ã' => 'a',
'ġ' => 'g',
'ṁ' => 'm',
'ō' => 'o',
'ĩ' => 'i',
'ù' => 'u',
'į' => 'i',
'ź' => 'z',
'á' => 'a',
'û' => 'u',
'þ' => 'th',
'ð' => 'dh',
'æ' => 'ae',
'µ' => 'u',
'ĕ' => 'e',
'œ' => 'oe',
];
/**
* Map of uppercased UTF-8 characters with their latin equivalents
*
* @var array
* @since 1.4.0
*/
private static $utf8UpperAccents = [
'À' => 'A',
'Ô' => 'O',
'Ď' => 'D',
'Ḟ' => 'F',
'Ë' => 'E',
'Š' => 'S',
'Ơ' => 'O',
'Ă' => 'A',
'Ř' => 'R',
'Ț' => 'T',
'Ň' => 'N',
'Ā' => 'A',
'Ķ' => 'K',
'Ŝ' => 'S',
'Ỳ' => 'Y',
'Ņ' => 'N',
'Ĺ' => 'L',
'Ħ' => 'H',
'Ṗ' => 'P',
'Ó' => 'O',
'Ú' => 'U',
'Ě' => 'E',
'É' => 'E',
'Ç' => 'C',
'Ẁ' => 'W',
'Ċ' => 'C',
'Õ' => 'O',
'Ṡ' => 'S',
'Ø' => 'O',
'Ģ' => 'G',
'Ŧ' => 'T',
'Ș' => 'S',
'Ė' => 'E',
'Ĉ' => 'C',
'Ś' => 'S',
'Î' => 'I',
'Ű' => 'U',
'Ć' => 'C',
'Ę' => 'E',
'Ŵ' => 'W',
'Ṫ' => 'T',
'Ū' => 'U',
'Č' => 'C',
'Ö' => 'Oe',
'È' => 'E',
'Ŷ' => 'Y',
'Ą' => 'A',
'Ł' => 'L',
'Ų' => 'U',
'Ů' => 'U',
'Ş' => 'S',
'Ğ' => 'G',
'Ļ' => 'L',
'Ƒ' => 'F',
'Ž' => 'Z',
'Ẃ' => 'W',
'Ḃ' => 'B',
'Å' => 'A',
'Ì' => 'I',
'Ï' => 'I',
'Ḋ' => 'D',
'Ť' => 'T',
'Ŗ' => 'R',
'Ä' => 'Ae',
'Í' => 'I',
'Ŕ' => 'R',
'Ê' => 'E',
'Ü' => 'Ue',
'Ò' => 'O',
'Ē' => 'E',
'Ñ' => 'N',
'Ń' => 'N',
'Ĥ' => 'H',
'Ĝ' => 'G',
'Đ' => 'D',
'Ĵ' => 'J',
'Ÿ' => 'Y',
'Ũ' => 'U',
'Ŭ' => 'U',
'Ư' => 'U',
'Ţ' => 'T',
'Ý' => 'Y',
'Ő' => 'O',
'Â' => 'A',
'Ľ' => 'L',
'Ẅ' => 'W',
'Ż' => 'Z',
'Ī' => 'I',
'Ã' => 'A',
'Ġ' => 'G',
'Ṁ' => 'M',
'Ō' => 'O',
'Ĩ' => 'I',
'Ù' => 'U',
'Į' => 'I',
'Ź' => 'Z',
'Á' => 'A',
'Û' => 'U',
'Þ' => 'Th',
'Ð' => 'Dh',
'Æ' => 'Ae',
'Ĕ' => 'E',
'Œ' => 'Oe',
];
/**
* Returns strings transliterated from UTF-8 to Latin
*
* @param string $string String to transliterate
* @param integer $case Optionally specify upper or lower case. Default to 0 (both).
*
* @return string Transliterated string
*
* @since 1.0
*/
public function utf8_latin_to_ascii($string, $case = 0)
{
if ($case <= 0) {
$string = str_replace(array_keys(self::$utf8LowerAccents), array_values(self::$utf8LowerAccents), $string);
}
if ($case >= 0) {
$string = str_replace(array_keys(self::$utf8UpperAccents), array_values(self::$utf8UpperAccents), $string);
}
return $string;
}
}