diff --git a/app/sprinkles/core/src/Bakery/LocaleFixKeysCommand.php b/app/sprinkles/core/src/Bakery/LocaleFixKeysCommand.php
new file mode 100644
index 000000000..cb9fcc209
--- /dev/null
+++ b/app/sprinkles/core/src/Bakery/LocaleFixKeysCommand.php
@@ -0,0 +1,243 @@
+setName('locale:fix-keys')
+ ->setHelp("This command generates missing keys for locale translation files. E.g. running 'locale:fix-keys -b en_US -f es_ES' will compare all es_ES and en_US locale files and populate es_ES with any missing keys from en_US.")
+ ->addOption('base', 'b', InputOption::VALUE_REQUIRED, 'The base locale used to generate values for any keys that are fixed. ', 'en_US')
+ ->addOption('locale', 'l', InputOption::VALUE_REQUIRED, 'One or more specific locales to fix. E.g. "fr_FR,es_ES" ', null)
+ ->addOption('force', 'f', InputOption::VALUE_NONE, 'Do not display confirmation.')
+ ->setDescription('Fix locale missing files and key values');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->title('Fixing Locale Keys');
+
+ // The "base" locale to compare other locales against. Defaults to en_US if not set.
+ $baseLocale = $input->getOption('base');
+ $baseLocaleFileNames = $this->getFilenames($baseLocale);
+
+ // Option -c. Set to compare one or more specific locales.
+ $locales = $input->getOption('locale');
+
+ // Get locales to fix
+ $localesToFix = $this->getLocales($baseLocale, $locales);
+
+ $this->io->note('Locales to be fixed: |' . implode('|', $localesToFix) . '|');
+
+ if (!$input->getOption('force') && !$this->io->confirm("All translation files for the locales above will be populated using key|values from | $baseLocale |. Continue?", false)) {
+ exit;
+ }
+
+ $fixed = [];
+
+ $progressBar = new ProgressBar($output);
+ $progressBar->start(count($localesToFix));
+
+ foreach ($localesToFix as $locale) {
+ $fixed[$locale] = $this->fixFiles($baseLocale, $locale, $baseLocaleFileNames);
+ $progressBar->advance();
+ }
+
+ $this->io->newLine(2);
+
+ $filesFixed = $this->getListValues($fixed);
+ if (empty($filesFixed)) {
+ $this->io->success('No file need fixing');
+ } else {
+ $this->io->section('Files fixed');
+ $this->io->listing($filesFixed);
+ $this->io->success('Files fixed successfully');
+ }
+ }
+
+ /**
+ * Build a list of files that were fixed.
+ *
+ * @param array $array File paths and missing keys.
+ *
+ * @return array A list of fixed files
+ */
+ protected function getListValues(array $array): array
+ {
+ $fixed = [];
+
+ foreach ($array as $key => $value) {
+ if (is_array($value)) {
+ //We need to loop through it.
+ $fixed = array_merge($fixed, $this->getListValues($value));
+ } elseif ($value != '0') {
+ //It is not an array and not '0', so add it to the list.
+ $fixed[] = $value;
+ }
+ }
+
+ return $fixed;
+ }
+
+ /**
+ * Iterate over sprinkle locale files and find the difference for two locales.
+ *
+ * @param string $baseLocale Locale being compared against.
+ * @param string $altLocale Locale to find missing keys for.
+ * @param array $filenames Sprinkle locale files that will be compared.
+ *
+ * @return array Filepaths that were fixed.
+ */
+ protected function fixFiles(string $baseLocale, string $altLocale, array $filenames): array
+ {
+ $fixed = [];
+
+ foreach ($filenames as $sprinklePath => $files) {
+ foreach ($files as $key => $file) {
+ $base = $this->parseFile("$sprinklePath/locale/$baseLocale/$file");
+ $alt = $this->parseFile("$sprinklePath/locale/$altLocale/$file");
+ $filePath = "$sprinklePath/locale/$altLocale/$file";
+ $diff = $this->getDifference($base, $alt);
+ $missing = $this->arrayFlatten($diff);
+
+ // The files with missing keys.
+ if (!empty($missing)) {
+ $fixed[] = $this->fix($base, $alt, $filePath, $missing);
+ }
+ }
+ }
+
+ return $fixed;
+ }
+
+ /**
+ * Fixes locale files by adding missing keys.
+ *
+ * @param array $base
+ * @param array $alt
+ * @param string $filePath The path of fixed file.
+ * @param array $missing
+ *
+ * @return string The path of the fixed file
+ */
+ protected function fix(array $base, array $alt, string $filePath, array $missing): string
+ {
+ //If the directory does not exist we need to create it recursively.
+ if (!file_exists(dirname($filePath))) {
+ mkdir(dirname($filePath), 0777, true);
+ }
+
+ // Build the respository and then merge in each locale file.
+ // Any keys not in the $alt locale will be the original left from the $base locales value.
+ $repository = new Repository();
+ $repository->mergeItems(null, $base);
+ $repository->mergeItems(null, $alt);
+
+ foreach ($missing as $key => $value) {
+ if (!$repository->has($key)) {
+ if (strpos($key, '@TRANSLATION') !== false) {
+ $val = $repository->get(str_replace('.@TRANSLATION', '', $key));
+ $repository->set($key, $val);
+ } else {
+ $repository->set($key, $value);
+ }
+ }
+ }
+
+ // Check if this is an existing locale file with docblock.
+ $temp = file_get_contents($filePath);
+
+ if (strpos($temp, '@author') !== false || strpos($temp, '/**') !== false) {
+ // Save existing docblock temporarily.
+ $start = strpos($temp, '/**');
+ $end = strpos(substr($temp, $start), '*/');
+ $docblock = file_get_contents($filePath, null, null, $start, $end + 2);
+
+ passthru("echo \ $filePath");
+
+ // We have to add the comment header prior to docblock or php-cs-fixer will overwrite it.
+ $this->fixFileWithPhpCs($filePath);
+
+ // Append the docblock after the header comment.
+ file_put_contents($filePath, $docblock, FILE_APPEND);
+ passthru("echo '\r\n' >> $filePath");
+ } else {
+ passthru("echo \ $filePath");
+ }
+
+ file_put_contents($filePath, var_export($repository->all(), true), FILE_APPEND);
+
+ passthru("echo \; >> $filePath");
+
+ // Final check with php-cs-fixer
+ $this->fixFileWithPhpCs($filePath);
+
+ // Insert 'return' into the file.
+ file_put_contents($filePath, preg_replace('/\[/', 'return [', file_get_contents($filePath), 1));
+
+ return $filePath;
+ }
+
+ /**
+ * Fix a file using php-cs-fixer.
+ *
+ * @param string $file path of file to fix
+ */
+ public function fixFileWithPhpCs(string $file): void
+ {
+ // Fix the file with php-cs-fixer
+ passthru("php ./app/vendor/friendsofphp/php-cs-fixer/php-cs-fixer fix $file --quiet --using-cache no --config ./.php_cs");
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getLocales(string $baseLocale, ?string $localesToCheck): array
+ {
+ $configuredLocales = array_diff(array_keys($this->ci->config['site']['locales']['available']), [$baseLocale]);
+
+ // If set, use the locale(s) from the -f option.
+ if ($localesToCheck) {
+ $locales = explode(',', $localesToCheck);
+ foreach ($locales as $key => $value) {
+ if (!in_array($value, $configuredLocales)) {
+ $this->io->warning("The |$value| locale was not found in your current configuration. Proceeding may results in a large number of files being created. Are you sure you want to continue?");
+ if (!$this->io->confirm('Continue?', false)) {
+ exit;
+ }
+ }
+ }
+
+ return $locales;
+ } else {
+ return $configuredLocales;
+ }
+ }
+}
diff --git a/app/sprinkles/core/src/Bakery/LocaleInfoCommand.php b/app/sprinkles/core/src/Bakery/LocaleInfoCommand.php
new file mode 100644
index 000000000..c9991d493
--- /dev/null
+++ b/app/sprinkles/core/src/Bakery/LocaleInfoCommand.php
@@ -0,0 +1,69 @@
+setName('locale:info')
+ ->setHelp('This command list all available locale as well as the defaut locale.')
+ ->setDescription('Informations about available locales');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->title('Available locales');
+
+ /** @var \UserFrosting\Sprinkle\Core\I18n\SiteLocale */
+ $localeService = $this->ci->locale;
+
+ // Get available locales
+ /** @var \UserFrosting\I18n\Locale[] $available */
+ $available = $localeService->getAvailable();
+
+ // Prepare table headers and lines array
+ $table = new Table($output);
+ $table->setHeaders(['Identifier', 'Name', 'Regional', 'Parents', 'Default']);
+
+ foreach ($available as $locale) {
+ $table->addRow([
+ $locale->getIdentifier(),
+ $locale->getName(),
+ $locale->getRegionalName(),
+ implode(', ', $locale->getDependentLocalesIdentifier()),
+ ($locale->getIdentifier() === $localeService->getDefaultLocale()) ? 'Yes' : '',
+ ]);
+ }
+
+ $table->render();
+
+ // Everything went fine, return 0 exit code
+ return 0;
+ }
+}
diff --git a/app/sprinkles/core/src/Bakery/LocaleMissingKeysCommand.php b/app/sprinkles/core/src/Bakery/LocaleMissingKeysCommand.php
new file mode 100644
index 000000000..ebfb1534a
--- /dev/null
+++ b/app/sprinkles/core/src/Bakery/LocaleMissingKeysCommand.php
@@ -0,0 +1,252 @@
+setName('locale:missing-keys')
+ ->setHelp("This command provides a summary of missing keys for locale translation files. E.g. running 'locale:missing-keys -b en_US -c es_ES' will compare all es_ES and en_US locale files and generate a table listing the filepath and missing keys found from the `-c` locale.")
+ ->addOption('base', 'b', InputOption::VALUE_REQUIRED, 'The base locale to compare against.', 'en_US')
+ ->addOption('check', 'c', InputOption::VALUE_REQUIRED, 'One or more specific locales to check. E.g. "fr_FR,es_ES"', null)
+ ->setDescription('Generate a table of missing locale keys.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->title('Missing Locale Keys');
+
+ // The "base" locale to compare other locales against. Defaults to en_US if not set.
+ $baseLocale = $input->getOption('base');
+
+ // Option -c. Set to compare one or more specific locales.
+ $localesToCheck = $input->getOption('check');
+
+ // Get locales to check
+ $locales = $this->getLocales($baseLocale, $localesToCheck);
+
+ $this->io->writeln('Locales to check: |' . implode('|', $locales) . '|');
+ $this->io->section("Searching for missing keys using $baseLocale for comparison.");
+
+ $difference = [];
+
+ foreach ($locales as $locale) {
+
+ // Make sure locale exist
+ if (!in_array($locale, array_keys($this->ci->config['site']['locales']['available']))) {
+ $this->io->warning("Locale '$locale' is not available in config.");
+ } else {
+ $difference = array_merge($difference, $this->compareFiles($baseLocale, $locale));
+ }
+ }
+
+ // Build the table.
+ if (!empty($difference)) {
+ $this->newTable($output);
+ $this->table->setHeaders(['File path', 'Missing key']);
+ $this->buildTable($difference);
+ $this->table->render();
+
+ $this->io->writeln('Missing keys found successfully');
+ } else {
+ $this->io->writeln('No missing keys found!');
+ }
+ }
+
+ /**
+ * Flattens a nested array into dot syntax.
+ *
+ * @param array $array The array to flatten.
+ * @param string $prefix (Default '')
+ *
+ * @return array Keys with missing values.
+ */
+ protected function arrayFlatten(array $array, string $prefix = ''): array
+ {
+ $result = [];
+ foreach ($array as $key => $value) {
+ if (is_array($value)) {
+ $result = $result + $this->arrayFlatten($value, $prefix . $key . '.');
+ } else {
+ $result[$prefix . $key] = $value;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Populate table with file paths and missing keys.
+ *
+ * @param array $array File paths and missing keys.
+ */
+ protected function buildTable(array $array): void
+ {
+ foreach ($array as $file => $missing) {
+ foreach ($missing as $key => $value) {
+ $this->table->addRow([$file, $key]);
+ }
+ }
+ }
+
+ /**
+ * Iterate over sprinkle locale files and find the difference for two locales.
+ *
+ * @param string $baseLocale Locale being compared against.
+ * @param string $altLocale Locale to find missing keys for.
+ *
+ * @return array The keys in $baseLocale that do not exist in $altLocale.
+ */
+ protected function compareFiles(string $baseLocale, string $altLocale): array
+ {
+ // Get all file for base locale
+ $files = $this->ci->locator->listResources("locale://$baseLocale", true);
+
+ // Return value
+ $difference = [];
+
+ foreach ($files as $basefile) {
+
+ // Get alt locale path
+ // Stream Path is used as security, in case a sprinkle would be called the same as a locale
+ $streamPath = $basefile->getStream()->getPath();
+ $altPath = str_replace("$streamPath/$baseLocale/", "$streamPath/$altLocale/", $basefile->getPath());
+
+ $base = $this->parseFile($basefile);
+ $alt = $this->parseFile($altPath);
+ $diff = $this->getDifference($base, $alt);
+
+ $difference[$altPath] = $this->arrayFlatten($diff);
+ }
+
+ return array_filter($difference);
+ }
+
+ /**
+ * Find the missing keys between two arrays.
+ *
+ * @param array $array1
+ * @param array $array2
+ *
+ * @return array
+ */
+ protected function getDifference(array $array1, array $array2): array
+ {
+ $difference = [];
+
+ foreach ($array1 as $key => $value) {
+ if (is_array($value)) {
+ if (!isset($array2[$key])) {
+ $difference[$key] = $value;
+ } else {
+ if (is_array($array2[$key])) {
+ $difference[$key] = $this->getDifference($value, $array2[$key]);
+ } else {
+ // If the second array returns a string for a key while
+ // the first is an array, the whole first array is considered missing
+ $difference[$key] = $value;
+ }
+ }
+ } elseif (!isset($array2[$key])) {
+ $difference[$key] = $value;
+ }
+ }
+
+ return $difference;
+ }
+
+ /**
+ * @param string $baseLocale The "base" locale to compare to
+ * @param string|null $localesToCheck Comma delimited list of locales to check
+ *
+ * @return array Locales to check.
+ */
+ protected function getLocales(string $baseLocale, ?string $localesToCheck): array
+ {
+ // If set, use the locale from the -c option.
+ if ($localesToCheck) {
+ return explode(',', $localesToCheck);
+ } else {
+ //Need to filter the base locale to prevent false positive.
+ return array_diff(array_keys($this->ci->config['site']['locales']['available']), [$baseLocale]);
+ }
+ }
+
+ /**
+ * Set up new table with Bakery formatting.
+ *
+ * @param OutputInterface $output
+ */
+ protected function newTable(OutputInterface $output): void
+ {
+ $tableStyle = new TableStyle();
+ $tableStyle->setVerticalBorderChars(' ')
+ ->setDefaultCrossingChar(' ')
+ ->setCellHeaderFormat('%s');
+
+ $this->table = new Table($output);
+ $this->table->setStyle($tableStyle);
+ }
+
+ /**
+ * Access file contents through inclusion.
+ *
+ * @param string $path The path of file to be included.
+ *
+ * @return array The array returned in the included locale file
+ */
+ protected function parseFile(string $path): array
+ {
+ // Return empty array if file not found
+ if (!file_exists($path)) {
+ return [];
+ }
+
+ $content = include "$path";
+
+ // Consider not found file returns an empty array
+ if ($content === false || !is_array($content)) {
+ return [];
+ }
+
+ return $content;
+ }
+}
diff --git a/app/sprinkles/core/src/Bakery/LocaleMissingValuesCommand.php b/app/sprinkles/core/src/Bakery/LocaleMissingValuesCommand.php
new file mode 100644
index 000000000..27d64d699
--- /dev/null
+++ b/app/sprinkles/core/src/Bakery/LocaleMissingValuesCommand.php
@@ -0,0 +1,281 @@
+setName('locale:missing-values')
+ ->setHelp("This command provides a summary of missing values for locale translation files. Missing keys are found by searching for empty and/or duplicate values. Either option can be turned off - see options for this command. E.g. running 'locale:missing-values -b en_US -c es_ES' will compare all es_ES and en_US locale files and find any values that are identical between the two locales, as well as searching all es_ES locale files for empty ('') values. This can be helpful to list all values in a specific languages that are present, but might need translation. For example, listing all English strings found in the French locale.")
+ ->addOption('base', 'b', InputOption::VALUE_REQUIRED, 'The base locale used for comparison and translation preview.', 'en_US')
+ ->addOption('check', 'c', InputOption::VALUE_REQUIRED, 'One or more specific locales to check. E.g. "fr_FR,es_ES"', null)
+ ->addOption('length', 'l', InputOption::VALUE_REQUIRED, 'Set the length for preview column text.', 50)
+ ->addOption('empty', 'e', InputOption::VALUE_NONE, 'Setting this will skip check for empty strings.')
+ ->addOption('duplicates', 'd', InputOption::VALUE_NONE, 'Setting this will skip comparison check.')
+ ->setDescription('Generate a table of keys with missing values.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->io->title('Missing Locale Values');
+
+ // Option -c. The locales to be checked.
+ $localesToCheck = $input->getOption('check');
+
+ // The locale for the 'preview' column. Defaults to en_US if not set.
+ $baseLocale = $input->getOption('base');
+ $baseLocaleFileNames = $this->getFilenames($baseLocale);
+
+ // Get `length` option
+ $length = $input->getOption('length');
+
+ // Set translator to base locale
+ $this->setTranslation($baseLocale);
+
+ // Get locales and files for said locales
+ $locales = $this->getLocales($baseLocale, $localesToCheck);
+ $files = $this->getFilePaths($locales);
+
+ $this->io->writeln(['Locales to check: |' . implode('|', $locales) . '|']);
+
+ // Proccess empty
+ if ($input->getOption('empty') === false) {
+ $this->io->section('Searching for empty values.');
+
+ $missing[] = $this->searchFilesForNull($files);
+
+ if (!empty($missing[0])) {
+ $this->newTable($output, $length);
+
+ $this->table->setHeaders([
+ ['File path', 'Key', 'Translation preview'],
+ ]);
+
+ // Build the table.
+ $this->buildTable($missing);
+
+ $this->table->render();
+ $this->io->newline(2);
+ } else {
+ $this->io->writeln('No empty values found!');
+ }
+ }
+
+ if ($input->getOption('duplicates') === false) {
+ $this->io->section('Searching for duplicate values.');
+
+ foreach ($locales as $locale) {
+ $duplicates[] = $this->compareFiles($baseLocale, $locale, $baseLocaleFileNames);
+ }
+
+ if (!empty($duplicates[0])) {
+ $this->newTable($output, $length);
+
+ $this->table->setHeaders([
+ ['File path', 'Key', 'Translation preview'],
+ ]);
+
+ $this->newTable($output, $length);
+ $this->table->setHeaders([
+ ['File path', 'Key', 'Duplicate value'],
+ ]);
+ $this->buildTable($duplicates);
+ $this->table->render();
+ } else {
+ $this->io->writeln('No empty values found!');
+ }
+ }
+ }
+
+ /**
+ * Intersect two arrays with considertaion of both keys and values.
+ *
+ * @param array $primary_array
+ * @param array $secondary_array
+ *
+ * @return array Matching keys and values that are found in both arrays.
+ */
+ protected function arrayIntersect(array $primary_array, array $secondary_array): array
+ {
+ if (!empty($primary_array)) {
+ foreach ($primary_array as $key => $value) {
+ if (!isset($secondary_array[$key])) {
+ unset($primary_array[$key]);
+ } else {
+ if (serialize($secondary_array[$key]) != serialize($value)) {
+ unset($primary_array[$key]);
+ }
+ }
+ }
+ // We only want empty values.
+ return array_filter($primary_array, function ($key) {
+ return strpos($key, '@') === false;
+ }, ARRAY_FILTER_USE_KEY);
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * Populate a table with data.
+ *
+ * @param array $array File paths and missing keys.
+ * @param int $level Nested array depth.
+ */
+ protected function buildTable(array $array, int $level = 1): void
+ {
+ foreach ($array as $key => $value) {
+ //Level 2 has the filepath.
+ if ($level == 2) {
+ // Make path easier to read by removing anything before 'sprinkles'
+ $this->path = strstr($key, 'sprinkles');
+ }
+ if (is_array($value)) {
+ //We need to loop through it.
+ $this->buildTable($value, ($level + 1));
+ } else {
+ $this->table->addRow([$this->path, $key, $this->translator->translate($key)]);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function compareFiles(string $baseLocale, string $altLocale, array $filenames): array
+ {
+ foreach ($filenames as $sprinklePath => $files) {
+ foreach ($files as $key => $file) {
+ $base = $this->arrayFlatten($this->parseFile("$sprinklePath/locale/$baseLocale/$file"));
+ $alt = $this->arrayFlatten($this->parseFile("$sprinklePath/locale/$altLocale/$file"));
+
+ $missing[$sprinklePath . '/locale' . '/' . $altLocale . '/' . $file] = $this->arrayIntersect($base, $alt);
+ }
+ }
+
+ return array_filter($missing);
+ }
+
+ /**
+ * Find keys with missing values.
+ * Collapses keys into array dot syntax.
+ * Missing values are identified using the same rules as the empty() method.
+ *
+ * @see https://www.php.net/manual/en/function.empty.php#refsect1-function.empty-returnvalues
+ *
+ * @param array $array Locale translation file.
+ * @param string $prefix
+ *
+ * @return array Keys with missing values.
+ */
+ protected function findMissing(array $array, string $prefix = ''): array
+ {
+ $result = [];
+ foreach ($array as $key => $value) {
+ if (is_array($value)) {
+ $result = $result + $this->findMissing($value, $prefix . $key . '.');
+ } else {
+ $result[$prefix . $key] = $value;
+ }
+ }
+
+ // We only want empty values.
+ return array_filter($result, function ($val, $key) {
+ return empty($val) && strpos($key, '@') === false;
+ }, ARRAY_FILTER_USE_BOTH);
+ }
+
+ /**
+ * Get a list of locale file paths.
+ *
+ * @param array $locale Array of locale(s) to get files for.
+ *
+ * @return array
+ */
+ protected function getFilePaths(array $locale): array
+ {
+ // Set up a locator class
+ $locator = $this->ci->locator;
+ $builder = new LocalePathBuilder($locator, 'locale://', $locale);
+ $loader = new ArrayFileLoader($builder->buildPaths());
+
+ // Get nested array [0].
+ return array_values((array) $loader)[0];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function newTable(OutputInterface $output, int $length): void
+ {
+ parent::newTable($output);
+ $this->table->setColumnMaxWidth(2, $length);
+ }
+
+ /**
+ * Search through locale files and find empty values.
+ *
+ * @param array $files File paths to search.
+ *
+ * @return array
+ */
+ protected function searchFilesForNull(array $files): array
+ {
+ foreach ($files as $file) {
+ $missing[$file] = $this->findMissing($this->parseFile($file));
+
+ if (empty($missing[$file])) {
+ unset($missing[$file]);
+ }
+ }
+
+ return $missing;
+ }
+
+ /**
+ * Sets up translator for a specific locale.
+ *
+ * @param string $locale Locale to be used for translation.
+ */
+ protected function setTranslation(string $locale): void
+ {
+ // Setup the translator. Set with -b or defaults to en_US
+ $locator = $this->ci->locator;
+ $builder = new LocalePathBuilder($locator, 'locale://', [$locale]);
+ $loader = new ArrayFileLoader($builder->buildPaths());
+ $this->translator = new MessageTranslator($loader->load());
+ }
+}
diff --git a/app/sprinkles/core/src/Util/EnvironmentInfo.php b/app/sprinkles/core/src/Util/EnvironmentInfo.php
index ae37080ed..8af658dd4 100644
--- a/app/sprinkles/core/src/Util/EnvironmentInfo.php
+++ b/app/sprinkles/core/src/Util/EnvironmentInfo.php
@@ -22,7 +22,7 @@
class EnvironmentInfo
{
/**
- * @var \Interop\Container\ContainerInterface The DI container for your application.
+ * @var \Psr\Container\ContainerInterface The DI container for your application.
*/
public static $ci;
diff --git a/app/sprinkles/core/tests/Integration/Bakery/LocaleInfoCommandTest.php b/app/sprinkles/core/tests/Integration/Bakery/LocaleInfoCommandTest.php
new file mode 100644
index 000000000..bbeee563b
--- /dev/null
+++ b/app/sprinkles/core/tests/Integration/Bakery/LocaleInfoCommandTest.php
@@ -0,0 +1,67 @@
+runCommand();
+ $this->assertSame(0, $result->getStatusCode());
+
+ $output = $result->getDisplay();
+ $this->assertNotContains('Français', $output);
+ $this->assertContains('English', $output);
+ }
+
+ /**
+ * @param string[] $input
+ */
+ protected function runCommand(array $input = []): CommandTester
+ {
+ // Force config to only three locales
+ $this->ci->config->set('site.locales.available', [
+ 'en_US' => true,
+ 'es_ES' => true,
+ 'fr_FR' => false,
+ ]);
+
+ // Create the app, create the command and add the command to the app
+ $app = new Application();
+ $command = new LocaleInfoCommand();
+ $command->setContainer($this->ci);
+ $app->add($command);
+
+ // Add the command to the input to create the execute argument
+ $execute = array_merge([
+ 'command' => $command->getName(),
+ ], $input);
+
+ // Execute command tester
+ $commandTester = new CommandTester($command);
+ $commandTester->execute($execute);
+
+ return $commandTester;
+ }
+}
diff --git a/app/sprinkles/core/tests/Integration/Bakery/LocaleMissingKeysCommandTest.php b/app/sprinkles/core/tests/Integration/Bakery/LocaleMissingKeysCommandTest.php
new file mode 100644
index 000000000..c6d7833fd
--- /dev/null
+++ b/app/sprinkles/core/tests/Integration/Bakery/LocaleMissingKeysCommandTest.php
@@ -0,0 +1,110 @@
+runCommand();
+
+ $output = $result->getDisplay();
+ $this->assertContains('Locales to check: |es_ES|fr_FR|', $output);
+ $this->assertContains('Missing keys found successfully', $output);
+ }
+
+ /**
+ * @depends testCommand
+ */
+ public function testCommandWithCheckArgument()
+ {
+ $result = $this->runCommand([
+ '--check' => 'fr_FR',
+ ]);
+
+ $output = $result->getDisplay();
+ $this->assertContains('Locales to check: |fr_FR|', $output);
+ $this->assertContains('app/sprinkles/core/tests/Integration/Bakery/data/locale/fr_FR/foo/bar.php', $output);
+ $this->assertContains('FOO.BAR', $output);
+ $this->assertContains('Missing keys found successfully', $output);
+ }
+
+ /**
+ * @depends testCommandWithCheckArgument
+ */
+ public function testCommandWithCheckArgumentNoMissingKeys()
+ {
+ $result = $this->runCommand([
+ '--check' => 'es_ES',
+ ]);
+
+ $output = $result->getDisplay();
+ $this->assertContains('Locales to check: |es_ES|', $output);
+ $this->assertContains('No missing keys found!', $output);
+ }
+
+ /**
+ * @param array $input
+ */
+ protected function runCommand($input = [])
+ {
+ // Replace default locale locator stream with the test data
+ $this->ci->locator->removeStream('locale');
+ $this->ci->locator->registerStream('locale', '', 'tests/Integration/Bakery/data/locale');
+
+ // Force config to only three locales
+ $this->ci->config->set('site.locales.available', [
+ 'en_US' => 'English',
+ 'es_ES' => 'Español',
+ 'fr_FR' => 'Français',
+ ]);
+
+ // Create the app, create the command and add the command to the app
+ $app = new Application();
+ $command = new LocaleMissingKeysCommand();
+ $command->setContainer($this->ci);
+ $app->add($command);
+
+ // Add the command to the input to create the execute argument
+ $execute = array_merge([
+ 'command' => $command->getName(),
+ ], $input);
+
+ // Execute command tester
+ $commandTester = new CommandTester($command);
+ $commandTester->execute($execute);
+
+ return $commandTester;
+ }
+}
diff --git a/app/sprinkles/core/tests/Integration/Bakery/data/locale/en_US/errors.php b/app/sprinkles/core/tests/Integration/Bakery/data/locale/en_US/errors.php
new file mode 100644
index 000000000..e48135fc0
--- /dev/null
+++ b/app/sprinkles/core/tests/Integration/Bakery/data/locale/en_US/errors.php
@@ -0,0 +1,40 @@
+ [
+ '@TRANSLATION' => 'Error',
+
+ '400' => [
+ 'TITLE' => 'Error 400: Bad Request',
+ 'DESCRIPTION' => "It's probably not your fault.",
+ ],
+
+ '404' => [
+ 'TITLE' => 'Error 404: Not Found',
+ 'DESCRIPTION' => "We can't seem to find what you're looking for.",
+ 'DETAIL' => 'We tried to find your page...',
+ 'EXPLAIN' => 'We could not find the page you were looking for.',
+ 'RETURN' => 'Either way, click here to return to the front page.',
+ ],
+
+ 'CONFIG' => [
+ 'TITLE' => 'UserFrosting Configuration Issue!',
+ 'DESCRIPTION' => 'Some UserFrosting configuration requirements have not been met.',
+ 'DETAIL' => "Something's not right here.",
+ 'RETURN' => 'Please fix the following errors, then reload.',
+ ],
+
+ 'DESCRIPTION' => "We've sensed a great disturbance in the Force.",
+ 'DETAIL' => "Here's what we got:",
+
+ 'ENCOUNTERED' => "Uhhh...something happened. We don't know what.",
+
+ 'MAIL' => 'Fatal error attempting mail, contact your server administrator. If you are the admin, please check the UserFrosting log.',
+
+ 'RETURN' => 'Click here to return to the front page.',
+
+ 'SERVER' => "Oops, looks like our server might have goofed. If you're an admin, please check the PHP or UserFrosting logs.",
+
+ 'TITLE' => 'Disturbance in the Force',
+ ],
+];
diff --git a/app/sprinkles/core/tests/Integration/Bakery/data/locale/en_US/foo/bar.php b/app/sprinkles/core/tests/Integration/Bakery/data/locale/en_US/foo/bar.php
new file mode 100644
index 000000000..ac9d9fa11
--- /dev/null
+++ b/app/sprinkles/core/tests/Integration/Bakery/data/locale/en_US/foo/bar.php
@@ -0,0 +1,7 @@
+ [
+ 'BAR' => 'Disturbance in the Force',
+ ],
+];
diff --git a/app/sprinkles/core/tests/Integration/Bakery/data/locale/en_US/messages.php b/app/sprinkles/core/tests/Integration/Bakery/data/locale/en_US/messages.php
new file mode 100644
index 000000000..3fd6d2cd6
--- /dev/null
+++ b/app/sprinkles/core/tests/Integration/Bakery/data/locale/en_US/messages.php
@@ -0,0 +1,109 @@
+ 1,
+
+ 'ABOUT' => 'About',
+
+ 'CAPTCHA' => [
+ '@TRANSLATION' => 'Captcha',
+ 'FAIL' => 'You did not enter the captcha code correctly.',
+ 'SPECIFY' => 'Enter the captcha',
+ 'VERIFY' => 'Verify the captcha',
+ ],
+
+ 'CSRF_MISSING' => 'Missing CSRF token. Try refreshing the page and then submitting again?',
+
+ 'DB_INVALID' => 'Cannot connect to the database. If you are an administrator, please check your error log.',
+ 'DESCRIPTION' => 'Description',
+ 'DOWNLOAD' => [
+ '@TRANSLATION' => 'Download',
+ 'CSV' => 'Download CSV',
+ ],
+
+ 'EMAIL' => [
+ '@TRANSLATION' => 'Email',
+ 'YOUR' => 'Your email address',
+ ],
+
+ 'HOME' => 'Home',
+
+ 'LEGAL' => [
+ '@TRANSLATION' => 'Legal Policy',
+ 'DESCRIPTION' => 'Our legal policy applies to your usage of this website and our services.',
+ ],
+
+ 'LOCALE' => [
+ '@TRANSLATION' => 'Locale',
+ ],
+
+ 'NAME' => 'Name',
+ 'NAVIGATION' => 'Navigation',
+ 'NO_RESULTS' => "Sorry, we've got nothing here.",
+
+ 'PAGINATION' => [
+ 'GOTO' => 'Jump to Page',
+ 'SHOW' => 'Show',
+
+ // Paginator
+ // possible variables: {size}, {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows}
+ // also {page:input} & {startRow:input} will add a modifiable input in place of the value
+ 'OUTPUT' => '{startRow} to {endRow} of {filteredRows} ({totalRows})',
+ 'NEXT' => 'Next page',
+ 'PREVIOUS' => 'Previous page',
+ 'FIRST' => 'First page',
+ 'LAST' => 'Last page',
+ ],
+ 'PRIVACY' => [
+ '@TRANSLATION' => 'Privacy Policy',
+ 'DESCRIPTION' => 'Our privacy policy outlines what kind of information we collect from you and how we will use it.',
+ ],
+
+ 'SLUG' => 'Slug',
+ 'SLUG_CONDITION' => 'Slug/Conditions',
+ 'SLUG_IN_USE' => 'A {{slug}} slug already exists',
+ 'STATUS' => 'Status',
+ 'SUGGEST' => 'Suggest',
+
+ 'UNKNOWN' => 'Unknown',
+
+ // Actions words
+ 'ACTIONS' => 'Actions',
+ 'ACTIVATE' => 'Activate',
+ 'ACTIVE' => 'Active',
+ 'ADD' => 'Add',
+ 'CANCEL' => 'Cancel',
+ 'CONFIRM' => 'Confirm',
+ 'CREATE' => 'Create',
+ 'DELETE' => 'Delete',
+ 'DELETE_CONFIRM' => 'Are you sure you want to delete this?',
+ 'DELETE_CONFIRM_YES' => 'Yes, delete',
+ 'DELETE_CONFIRM_NAMED' => 'Are you sure you want to delete {{name}}?',
+ 'DELETE_CONFIRM_YES_NAMED' => 'Yes, delete {{name}}',
+ 'DELETE_CANNOT_UNDONE' => 'This action cannot be undone.',
+ 'DELETE_NAMED' => 'Delete {{name}}',
+ 'DENY' => 'Deny',
+ 'DISABLE' => 'Disable',
+ 'DISABLED' => 'Disabled',
+ 'EDIT' => 'Edit',
+ 'ENABLE' => 'Enable',
+ 'ENABLED' => 'Enabled',
+ 'OVERRIDE' => 'Override',
+ 'RESET' => 'Reset',
+ 'SAVE' => 'Save',
+ 'SEARCH' => 'Search',
+ 'SORT' => 'Sort',
+ 'SUBMIT' => 'Submit',
+ 'PRINT' => 'Print',
+ 'REMOVE' => 'Remove',
+ 'UNACTIVATED' => 'Unactivated',
+ 'UPDATE' => 'Update',
+ 'YES' => 'Yes',
+ 'NO' => 'No',
+ 'OPTIONAL' => 'Optional',
+
+ // Misc.
+ 'BUILT_WITH_UF' => 'Built with UserFrosting',
+ 'ADMINLTE_THEME_BY' => 'Theme by Almsaeed Studio. All rights reserved',
+ 'WELCOME_TO' => 'Welcome to {{title}}!',
+];
diff --git a/app/sprinkles/core/tests/Integration/Bakery/data/locale/es_ES/errors.php b/app/sprinkles/core/tests/Integration/Bakery/data/locale/es_ES/errors.php
new file mode 100644
index 000000000..e48135fc0
--- /dev/null
+++ b/app/sprinkles/core/tests/Integration/Bakery/data/locale/es_ES/errors.php
@@ -0,0 +1,40 @@
+ [
+ '@TRANSLATION' => 'Error',
+
+ '400' => [
+ 'TITLE' => 'Error 400: Bad Request',
+ 'DESCRIPTION' => "It's probably not your fault.",
+ ],
+
+ '404' => [
+ 'TITLE' => 'Error 404: Not Found',
+ 'DESCRIPTION' => "We can't seem to find what you're looking for.",
+ 'DETAIL' => 'We tried to find your page...',
+ 'EXPLAIN' => 'We could not find the page you were looking for.',
+ 'RETURN' => 'Either way, click here to return to the front page.',
+ ],
+
+ 'CONFIG' => [
+ 'TITLE' => 'UserFrosting Configuration Issue!',
+ 'DESCRIPTION' => 'Some UserFrosting configuration requirements have not been met.',
+ 'DETAIL' => "Something's not right here.",
+ 'RETURN' => 'Please fix the following errors, then reload.',
+ ],
+
+ 'DESCRIPTION' => "We've sensed a great disturbance in the Force.",
+ 'DETAIL' => "Here's what we got:",
+
+ 'ENCOUNTERED' => "Uhhh...something happened. We don't know what.",
+
+ 'MAIL' => 'Fatal error attempting mail, contact your server administrator. If you are the admin, please check the UserFrosting log.',
+
+ 'RETURN' => 'Click here to return to the front page.',
+
+ 'SERVER' => "Oops, looks like our server might have goofed. If you're an admin, please check the PHP or UserFrosting logs.",
+
+ 'TITLE' => 'Disturbance in the Force',
+ ],
+];
diff --git a/app/sprinkles/core/tests/Integration/Bakery/data/locale/es_ES/foo/bar.php b/app/sprinkles/core/tests/Integration/Bakery/data/locale/es_ES/foo/bar.php
new file mode 100644
index 000000000..ac9d9fa11
--- /dev/null
+++ b/app/sprinkles/core/tests/Integration/Bakery/data/locale/es_ES/foo/bar.php
@@ -0,0 +1,7 @@
+ [
+ 'BAR' => 'Disturbance in the Force',
+ ],
+];
diff --git a/app/sprinkles/core/tests/Integration/Bakery/data/locale/es_ES/messages.php b/app/sprinkles/core/tests/Integration/Bakery/data/locale/es_ES/messages.php
new file mode 100644
index 000000000..3fd6d2cd6
--- /dev/null
+++ b/app/sprinkles/core/tests/Integration/Bakery/data/locale/es_ES/messages.php
@@ -0,0 +1,109 @@
+ 1,
+
+ 'ABOUT' => 'About',
+
+ 'CAPTCHA' => [
+ '@TRANSLATION' => 'Captcha',
+ 'FAIL' => 'You did not enter the captcha code correctly.',
+ 'SPECIFY' => 'Enter the captcha',
+ 'VERIFY' => 'Verify the captcha',
+ ],
+
+ 'CSRF_MISSING' => 'Missing CSRF token. Try refreshing the page and then submitting again?',
+
+ 'DB_INVALID' => 'Cannot connect to the database. If you are an administrator, please check your error log.',
+ 'DESCRIPTION' => 'Description',
+ 'DOWNLOAD' => [
+ '@TRANSLATION' => 'Download',
+ 'CSV' => 'Download CSV',
+ ],
+
+ 'EMAIL' => [
+ '@TRANSLATION' => 'Email',
+ 'YOUR' => 'Your email address',
+ ],
+
+ 'HOME' => 'Home',
+
+ 'LEGAL' => [
+ '@TRANSLATION' => 'Legal Policy',
+ 'DESCRIPTION' => 'Our legal policy applies to your usage of this website and our services.',
+ ],
+
+ 'LOCALE' => [
+ '@TRANSLATION' => 'Locale',
+ ],
+
+ 'NAME' => 'Name',
+ 'NAVIGATION' => 'Navigation',
+ 'NO_RESULTS' => "Sorry, we've got nothing here.",
+
+ 'PAGINATION' => [
+ 'GOTO' => 'Jump to Page',
+ 'SHOW' => 'Show',
+
+ // Paginator
+ // possible variables: {size}, {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows}
+ // also {page:input} & {startRow:input} will add a modifiable input in place of the value
+ 'OUTPUT' => '{startRow} to {endRow} of {filteredRows} ({totalRows})',
+ 'NEXT' => 'Next page',
+ 'PREVIOUS' => 'Previous page',
+ 'FIRST' => 'First page',
+ 'LAST' => 'Last page',
+ ],
+ 'PRIVACY' => [
+ '@TRANSLATION' => 'Privacy Policy',
+ 'DESCRIPTION' => 'Our privacy policy outlines what kind of information we collect from you and how we will use it.',
+ ],
+
+ 'SLUG' => 'Slug',
+ 'SLUG_CONDITION' => 'Slug/Conditions',
+ 'SLUG_IN_USE' => 'A {{slug}} slug already exists',
+ 'STATUS' => 'Status',
+ 'SUGGEST' => 'Suggest',
+
+ 'UNKNOWN' => 'Unknown',
+
+ // Actions words
+ 'ACTIONS' => 'Actions',
+ 'ACTIVATE' => 'Activate',
+ 'ACTIVE' => 'Active',
+ 'ADD' => 'Add',
+ 'CANCEL' => 'Cancel',
+ 'CONFIRM' => 'Confirm',
+ 'CREATE' => 'Create',
+ 'DELETE' => 'Delete',
+ 'DELETE_CONFIRM' => 'Are you sure you want to delete this?',
+ 'DELETE_CONFIRM_YES' => 'Yes, delete',
+ 'DELETE_CONFIRM_NAMED' => 'Are you sure you want to delete {{name}}?',
+ 'DELETE_CONFIRM_YES_NAMED' => 'Yes, delete {{name}}',
+ 'DELETE_CANNOT_UNDONE' => 'This action cannot be undone.',
+ 'DELETE_NAMED' => 'Delete {{name}}',
+ 'DENY' => 'Deny',
+ 'DISABLE' => 'Disable',
+ 'DISABLED' => 'Disabled',
+ 'EDIT' => 'Edit',
+ 'ENABLE' => 'Enable',
+ 'ENABLED' => 'Enabled',
+ 'OVERRIDE' => 'Override',
+ 'RESET' => 'Reset',
+ 'SAVE' => 'Save',
+ 'SEARCH' => 'Search',
+ 'SORT' => 'Sort',
+ 'SUBMIT' => 'Submit',
+ 'PRINT' => 'Print',
+ 'REMOVE' => 'Remove',
+ 'UNACTIVATED' => 'Unactivated',
+ 'UPDATE' => 'Update',
+ 'YES' => 'Yes',
+ 'NO' => 'No',
+ 'OPTIONAL' => 'Optional',
+
+ // Misc.
+ 'BUILT_WITH_UF' => 'Built with UserFrosting',
+ 'ADMINLTE_THEME_BY' => 'Theme by Almsaeed Studio. All rights reserved',
+ 'WELCOME_TO' => 'Welcome to {{title}}!',
+];
diff --git a/app/sprinkles/core/tests/Integration/Bakery/data/locale/fr_FR/errors.php b/app/sprinkles/core/tests/Integration/Bakery/data/locale/fr_FR/errors.php
new file mode 100644
index 000000000..a7275ef8c
--- /dev/null
+++ b/app/sprinkles/core/tests/Integration/Bakery/data/locale/fr_FR/errors.php
@@ -0,0 +1,40 @@
+ [
+ '@TRANSLATION' => 'Erreur',
+
+ '400' => [
+ 'TITLE' => 'Erreur 400: Mauvaise requête',
+ 'DESCRIPTION' => "Ce n'est probablement pas de votre faute.",
+ ],
+
+ '404' => [
+ 'TITLE' => 'Erreur 404: Page introuvable',
+ 'DESCRIPTION' => 'Nous ne pouvons trouver ce que vous cherchez.',
+ 'DETAIL' => 'Nous avons tout tenté...',
+ 'EXPLAIN' => 'Nous ne pouvons trouver la page que vous cherchez.',
+ 'RETURN' => 'Cliquez ici pour retourner à la page d\'accueil.',
+ ],
+
+ 'CONFIG' => [
+ 'TITLE' => 'Problème de configuration UserFrosting!',
+ 'DESCRIPTION' => "Les exigences de configuration de UserFrosting n'ont pas été satisfaites.",
+ 'DETAIL' => 'Quelque chose cloche ici...',
+ 'RETURN' => 'Corrigez les erreurs suivantes, ensuite recharger la page.',
+ ],
+
+ 'DESCRIPTION' => 'Nous avons ressenti un grand bouleversement de la Force.',
+ 'DETAIL' => 'Voici les détails :',
+
+ 'ENCOUNTERED' => "D'oh! Quelque chose s'est produit. Aucune idée c'est quoi.",
+
+ 'MAIL' => "Erreur fatale lors de l'envoie du courriel. Contactez votre administrateur. Si vous être administrateur, consultez les logs.",
+
+ 'RETURN' => 'Cliquez ici pour retourner à la page d\'accueil.',
+
+ 'SERVER' => "Oops, il semblerait que le serveur a gaffé. Si vous êtes administrateur, s-v-p vérifier les logs d'erreurs PHP ou ceux de UserFrosting.",
+
+ 'TITLE' => 'Bouleversement de la Force',
+ ],
+];
diff --git a/app/sprinkles/core/tests/Integration/Bakery/data/locale/fr_FR/messages.php b/app/sprinkles/core/tests/Integration/Bakery/data/locale/fr_FR/messages.php
new file mode 100644
index 000000000..56c410c29
--- /dev/null
+++ b/app/sprinkles/core/tests/Integration/Bakery/data/locale/fr_FR/messages.php
@@ -0,0 +1,94 @@
+ 2,
+
+ 'ABOUT' => 'À propos',
+
+ 'CAPTCHA' => [
+ '@TRANSLATE' => 'Captcha',
+ 'VERIFY' => 'Vérification du captcha',
+ 'SPECIFY' => 'Entrer la valeur du captcha',
+ 'FAIL' => "La valeur du captcha n'a pas été entrée correctement.",
+ ],
+
+ 'CSRF_MISSING' => 'Jeton CSRF manquant. Essayez de rafraîchir la page et de soumettre de nouveau?',
+
+ 'DB_INVALID' => "Impossible de se connecter à la base de données. Si vous êtes un administrateur, vérifiez votre journal d'erreurs.",
+ 'DESCRIPTION' => 'Description',
+ 'DOWNLOAD' => [
+ '@TRANSLATION' => 'Télécharger',
+ 'CSV' => 'Télécharger CSV',
+ ],
+
+ 'EMAIL' => [
+ '@TRANSLATION' => 'Email',
+ 'YOUR' => 'Votre adresse email',
+ ],
+
+ 'HOME' => 'Accueil',
+
+ 'LEGAL' => 'Politique légale',
+
+ 'LOCALE' => [
+ '@TRANSLATION' => 'Langue',
+ ],
+
+ 'NAME' => 'Nom',
+ 'NAVIGATION' => 'Menu principal',
+ 'NO_RESULTS' => 'Aucun résultat trouvé.',
+
+ 'PAGINATION' => [
+ 'GOTO' => 'Aller à la page',
+ 'SHOW' => 'Afficher',
+ 'OUTPUT' => '{startRow} à {endRow} de {filteredRows} ({totalRows})',
+ ],
+ 'PRIVACY' => 'Politique de confidentialité',
+
+ 'SLUG' => 'Jeton',
+ 'SLUG_CONDITION' => 'Jeton/Conditions',
+ 'SLUG_IN_USE' => 'Un jeton {{slug}} existe déjà',
+ 'STATUS' => 'Statut',
+ 'SUGGEST' => 'Suggérer',
+
+ 'UNKNOWN' => 'Inconnu',
+
+ // Actions words
+ 'ACTIONS' => 'Actions',
+ 'ACTIVATE' => 'Autoriser',
+ 'ACTIVE' => 'Activé',
+ 'ADD' => 'Ajouter',
+ 'CANCEL' => 'Annuler',
+ 'CONFIRM' => 'Confirmer',
+ 'CREATE' => 'Créer',
+ 'DELETE' => 'Supprimer',
+ 'DELETE_CONFIRM' => 'Êtes-vous sûr de vouloir supprimer ceci?',
+ 'DELETE_CONFIRM_YES' => 'Oui, supprimer',
+ 'DELETE_CONFIRM_NAMED' => 'Êtes-vous sûr de vouloir supprimer {{name}}?',
+ 'DELETE_CONFIRM_YES_NAMED' => 'Oui, supprimer {{name}}',
+ 'DELETE_CANNOT_UNDONE' => 'Cette action ne peut être annulée.', //This action cannot be undone
+ 'DELETE_NAMED' => 'Supprimer {{name}}',
+ 'DENY' => 'Refuser',
+ 'DISABLE' => 'Désactiver',
+ 'DISABLED' => 'Désactivé',
+ 'EDIT' => 'Modifier',
+ 'ENABLE' => 'Activer',
+ 'ENABLED' => 'Activé',
+ 'OVERRIDE' => 'Forcer',
+ 'RESET' => 'Réinitialiser',
+ 'SAVE' => 'Sauvegarder',
+ 'SEARCH' => 'Rechercher',
+ 'SORT' => 'Trier',
+ 'SUBMIT' => 'Envoyer',
+ 'PRINT' => 'Imprimer',
+ 'REMOVE' => 'Supprimer',
+ 'UNACTIVATED' => 'Non activé',
+ 'UPDATE' => 'Mettre à jour',
+ 'YES' => 'Oui',
+ 'NO' => 'Non',
+ 'OPTIONAL' => 'Facultatif',
+
+ // Misc.
+ 'BUILT_WITH_UF' => 'Créé avec UserFrosting',
+ 'ADMINLTE_THEME_BY' => 'Thème par Almsaeed Studio. Tous droits réservés',
+];
diff --git a/app/system/Bakery/Bakery.php b/app/system/Bakery/Bakery.php
index 1d8fe1a48..45a48136a 100644
--- a/app/system/Bakery/Bakery.php
+++ b/app/system/Bakery/Bakery.php
@@ -31,7 +31,7 @@ class Bakery
protected $app;
/**
- * @var \Interop\Container\ContainerInterface The global container object, which holds all your services.
+ * @var \Psr\Container\ContainerInterface The global container object, which holds all your services.
*/
protected $ci;
diff --git a/app/system/Bakery/BaseCommand.php b/app/system/Bakery/BaseCommand.php
index 54dc198cc..e91fe136a 100644
--- a/app/system/Bakery/BaseCommand.php
+++ b/app/system/Bakery/BaseCommand.php
@@ -24,13 +24,14 @@
abstract class BaseCommand extends Command
{
/**
- * @var \Symfony\Component\Console\Style\SymfonyStyle
- * See http://symfony.com/doc/current/console/style.html
+ * @var \Symfony\Component\Console\Style\SymfonyStyle
+ *
+ * @see http://symfony.com/doc/current/console/style.html
*/
protected $io;
/**
- * @var ContainerInterface The global container object, which holds all of UserFrosting services.
+ * @var ContainerInterface The global container object, which holds all of UserFrosting services.
*/
protected $ci;
@@ -47,7 +48,7 @@ protected function initialize(InputInterface $input, OutputInterface $output)
*
* @param ContainerInterface $ci
*/
- public function setContainer(ContainerInterface $ci)
+ public function setContainer(ContainerInterface $ci): void
{
$this->ci = $ci;
}
@@ -57,12 +58,12 @@ public function setContainer(ContainerInterface $ci)
*
* @return bool True/False if the app is in production mode
*/
- protected function isProduction()
+ protected function isProduction(): bool
{
- // N.B.: Need to touch the config service first to load dotenv values
+ // Need to touch the config service first to load dotenv values
$config = $this->ci->config;
$mode = getenv('UF_MODE') ?: '';
- return $mode == 'production';
+ return $mode === 'production';
}
}
diff --git a/app/tests/TestCase.php b/app/tests/TestCase.php
index f3d82f1c5..f9a88658e 100644
--- a/app/tests/TestCase.php
+++ b/app/tests/TestCase.php
@@ -24,7 +24,7 @@ class TestCase extends BaseTestCase
/**
* The global container object, which holds all your services.
*
- * @var \Interop\Container\ContainerInterface
+ * @var \Psr\Container\ContainerInterface
*/
protected $ci;