Repository: codeigniter4/CodeIgniter4 (PHP)

Issue:
Translation function 'lang' does not work as described
In the documentation you are describing the lang function to work this way:
```
// Displays "£7.41"
echo lang('{price, number, currency}', ['price' => 7.41], 'en-GB');
```

However this doesn't work in the current release (4.0.4).

It just echos the string: {price, number, currency}

If I comment out the lines 140 - 143 inside the file framework/system/Language/Language.php
```
// ignore requests with no file specified
		/*if (! strpos($line, '.'))
		{
			return $line;
		}*/
```

it works as described and the output is: £7.41



Discussion:
I believe this is more of a documentation issue than a bug in the code. The `lang` function is intended to work with language strings inside files which are loaded by the function.
Is that so?
It would be much more comfortable (and makes more sense to me) if it would work as described in the documentation and not in the way that every string has to be a language file.

However either this or that needs a revision.
Per code, the parsing seems to support only file-based language strings. But you are right. The example in the documentation is most appropriate for single uses.

Any thoughts, @MGatner , @michalsn , @samsonasik ?



Patch:
diff --git a/system/Language/Language.php b/system/Language/Language.php
index bfc25e70c021..f812af8b52c0 100644
--- a/system/Language/Language.php
+++ b/system/Language/Language.php
@@ -21,7 +21,6 @@
  */
 class Language
 {
-
 	/**
 	 * Stores the retrieved language lines
 	 * from files for faster retrieval on
@@ -60,10 +59,10 @@ public function __construct(string $locale)
 	{
 		$this->locale = $locale;
 
-		if (class_exists('\MessageFormatter'))
+		if (class_exists('MessageFormatter'))
 		{
 			$this->intlSupport = true;
-		};
+		}
 	}
 
 	//--------------------------------------------------------------------
@@ -108,29 +107,23 @@ public function getLocale(): string
 	 */
 	public function getLine(string $line, array $args = [])
 	{
-		// ignore requests with no file specified
-		if (! strpos($line, '.'))
+		// if no file is given, just parse the line
+		if (strpos($line, '.') === false)
 		{
-			return $line;
+			return $this->formatMessage($line, $args);
 		}
 
 		// Parse out the file name and the actual alias.
 		// Will load the language file and strings.
-		[
-			$file,
-			$parsedLine,
-		] = $this->parseLine($line, $this->locale);
+		list($file, $parsedLine) = $this->parseLine($line, $this->locale);
 
 		$output = $this->getTranslationOutput($this->locale, $file, $parsedLine);
 
 		if ($output === null && strpos($this->locale, '-'))
 		{
-			[$locale] = explode('-', $this->locale, 2);
+			list($locale) = explode('-', $this->locale, 2);
 
-			[
-				$file,
-				$parsedLine,
-			] = $this->parseLine($line, $locale);
+			list($file, $parsedLine) = $this->parseLine($line, $locale);
 
 			$output = $this->getTranslationOutput($locale, $file, $parsedLine);
 		}
@@ -138,21 +131,14 @@ public function getLine(string $line, array $args = [])
 		// if still not found, try English
 		if ($output === null)
 		{
-			[
-				$file,
-				$parsedLine,
-			]       = $this->parseLine($line, 'en');
+			list($file, $parsedLine) = $this->parseLine($line, 'en');
+
 			$output = $this->getTranslationOutput('en', $file, $parsedLine);
 		}
 
 		$output = $output ?? $line;
 
-		if (! empty($args))
-		{
-			$output = $this->formatMessage($output, $args);
-		}
-
-		return $output;
+		return $this->formatMessage($output, $args);
 	}
 
 	//--------------------------------------------------------------------
@@ -230,7 +216,7 @@ protected function parseLine(string $line, string $locale): array
 	 */
 	protected function formatMessage($message, array $args = [])
 	{
-		if (! $this->intlSupport || ! $args)
+		if (! $this->intlSupport || $args === [])
 		{
 			return $message;
 		}
@@ -241,6 +227,7 @@ protected function formatMessage($message, array $args = [])
 			{
 				$message[$index] = $this->formatMessage($value, $args);
 			}
+
 			return $message;
 		}
 


Test patch:
diff --git a/tests/system/Language/LanguageTest.php b/tests/system/Language/LanguageTest.php
index 8babb6773085..3849949bc111 100644
--- a/tests/system/Language/LanguageTest.php
+++ b/tests/system/Language/LanguageTest.php
@@ -1,74 +1,83 @@
 <?php
+
 namespace CodeIgniter\Language;
 
+use CodeIgniter\Test\CIUnitTestCase;
 use CodeIgniter\Test\Mock\MockLanguage;
 use Config\Services;
 use Tests\Support\Language\SecondMockLanguage;
 
-class LanguageTest extends \CodeIgniter\Test\CIUnitTestCase
+class LanguageTest extends CIUnitTestCase
 {
+	/**
+	 * @var MockLanguage
+	 */
+	private $lang;
+
+	protected function setUp(): void
+	{
+		$this->lang = new MockLanguage('en');
+	}
 
 	public function testReturnsStringWithNoFileInMessage()
 	{
-		$lang = new MockLanguage('en');
+		$this->assertEquals('something', $this->lang->getLine('something'));
+	}
+
+	/**
+	 * @see https://github.com/codeigniter4/CodeIgniter4/issues/3822
+	 */
+	public function testReturnParsedStringWithNoFileInMessage()
+	{
+		$this->lang->setLocale('en-GB');
+
+		$line = '{price, number, currency}';
 
-		$this->assertEquals('something', $lang->getLine('something'));
+		$this->assertSame('£7.41', $this->lang->getLine($line, ['price' => '7.41']));
+		$this->assertSame('£7.41', lang($line, ['price' => '7.41'], 'en-GB'));
 	}
 
 	//--------------------------------------------------------------------
 
 	public function testGetLineReturnsLine()
 	{
-		$lang = new MockLanguage('en');
-
-		$lang->setData('books', [
+		$this->lang->setData('books', [
 			'bookSaved'  => 'We kept the book free from the boogeyman',
 			'booksSaved' => 'We saved some more',
 		]);
 
-		$this->assertEquals('We saved some more', $lang->getLine('books.booksSaved'));
+		$this->assertEquals('We saved some more', $this->lang->getLine('books.booksSaved'));
 	}
 
 	//--------------------------------------------------------------------
 
 	public function testGetLineReturnsFallbackLine()
 	{
-		$lang = new MockLanguage('en-US');
-		$lang->setData('equivalent', [
-			'touchWood'   => 'touch wood',
-			'lieOfLand'   => 'lie of the land',
-			'leaseOfLife' => 'a new lease of life',
-			'slowcoach'   => 'slowcoach',
-		], 'en');
-		$lang->setData('equivalent', [
-			'lieOfLand' => 'lay of the land',
-			'slowcoach' => 'slowpoke',
-		], 'en-US');
-
-		$this->assertEquals(
-				'lay of the land', $lang->getLine('equivalent.lieOfLand')
-		);
-		$this->assertEquals(
-				'slowpoke', $lang->getLine('equivalent.slowcoach')
-		);
-		$this->assertEquals(
-				'a new lease of life', $lang->getLine('equivalent.leaseOfLife')
-		);
-		$this->assertEquals(
-				'touch wood', $lang->getLine('equivalent.touchWood')
-		);
-		$this->assertEquals(
-				'equivalent.unknown', $lang->getLine('equivalent.unknown')
-		);
+		$this->lang
+			->setLocale('en-US')
+			->setData('equivalent', [
+				'touchWood'   => 'touch wood',
+				'lieOfLand'   => 'lie of the land',
+				'leaseOfLife' => 'a new lease of life',
+				'slowcoach'   => 'slowcoach',
+			], 'en')
+			->setData('equivalent', [
+				'lieOfLand' => 'lay of the land',
+				'slowcoach' => 'slowpoke',
+			], 'en-US');
+
+		$this->assertEquals('lay of the land', $this->lang->getLine('equivalent.lieOfLand'));
+		$this->assertEquals('slowpoke', $this->lang->getLine('equivalent.slowcoach'));
+		$this->assertEquals('a new lease of life', $this->lang->getLine('equivalent.leaseOfLife'));
+		$this->assertEquals('touch wood', $this->lang->getLine('equivalent.touchWood'));
+		$this->assertEquals('equivalent.unknown', $this->lang->getLine('equivalent.unknown'));
 	}
 
 	//--------------------------------------------------------------------
 
 	public function testGetLineArrayReturnsLineArray()
 	{
-		$lang = new MockLanguage('en');
-
-		$lang->setData('books', [
+		$this->lang->setData('books', [
 			'booksList' => [
 				'The Boogeyman',
 				'We Saved',
@@ -78,7 +87,7 @@ public function testGetLineArrayReturnsLineArray()
 		$this->assertEquals([
 			'The Boogeyman',
 			'We Saved',
-		], $lang->getLine('books.booksList'));
+		], $this->lang->getLine('books.booksList'));
 	}
 
 	//--------------------------------------------------------------------
@@ -86,18 +95,16 @@ public function testGetLineArrayReturnsLineArray()
 	public function testGetLineFormatsMessage()
 	{
 		// No intl extension? then we can't test this - go away....
-		if (! class_exists('\MessageFormatter'))
+		if (! class_exists('MessageFormatter'))
 		{
-			return;
+			$this->markTestSkipped('No intl support.');
 		}
 
-		$lang = new MockLanguage('en');
-
-		$lang->setData('books', [
+		$this->lang->setData('books', [
 			'bookCount' => '{0, number, integer} books have been saved.',
 		]);
 
-		$this->assertEquals('45 books have been saved.', $lang->getLine('books.bookCount', [91 / 2]));
+		$this->assertEquals('45 books have been saved.', $this->lang->getLine('books.bookCount', [91 / 2]));
 	}
 
 	//--------------------------------------------------------------------
@@ -105,20 +112,18 @@ public function testGetLineFormatsMessage()
 	public function testGetLineArrayFormatsMessages()
 	{
 		// No intl extension? Then we can't test this - go away...
-		if (! class_exists('\MessageFormatter'))
+		if (! class_exists('MessageFormatter'))
 		{
-			return;
+			$this->markTestSkipped('No intl support.');
 		}
 
-		$lang = new MockLanguage('en');
-
-		$lang->setData('books', [
+		$this->lang->setData('books', [
 			'bookList' => [
 				'{0, number, integer} related books.'
 			],
 		]);
 
-		$this->assertEquals(['45 related books.'], $lang->getLine('books.bookList', [91 / 2]));
+		$this->assertEquals(['45 related books.'], $this->lang->getLine('books.bookList', [91 / 2]));
 	}
 
 	//--------------------------------------------------------------------
@@ -139,79 +144,78 @@ public function testLangAllowsOtherLocales()
 
 	public function testLangDoesntFormat()
 	{
-		$lang = new MockLanguage('en');
-		$lang->disableIntlSupport();
+		$this->lang->disableIntlSupport();
 
-		$lang->setData('books', [
+		$this->lang->setData('books', [
 			'bookList' => [
 				'{0, number, integer} related books.'
 			],
 		]);
 
-		$this->assertEquals(['{0, number, integer} related books.'], $lang->getLine('books.bookList', [15]));
+		$this->assertEquals(['{0, number, integer} related books.'], $this->lang->getLine('books.bookList', [15]));
 	}
 
 	//--------------------------------------------------------------------
 
 	public function testLanguageDuplicateKey()
 	{
-		$lang = new Language('en', false);
-		$this->assertEquals('These are not the droids you are looking for', $lang->getLine('More.strongForce', []));
-		$this->assertEquals('I have a very bad feeling about this', $lang->getLine('More.cannotMove', []));
-		$this->assertEquals('Could not move file {0} to {1} ({2})', $lang->getLine('Files.cannotMove', []));
-		$this->assertEquals('I have a very bad feeling about this', $lang->getLine('More.cannotMove', []));
+		$this->lang = new Language('en');
+		$this->assertEquals('These are not the droids you are looking for', $this->lang->getLine('More.strongForce', []));
+		$this->assertEquals('I have a very bad feeling about this', $this->lang->getLine('More.cannotMove', []));
+		$this->assertEquals('Could not move file {0} to {1} ({2})', $this->lang->getLine('Files.cannotMove', []));
+		$this->assertEquals('I have a very bad feeling about this', $this->lang->getLine('More.cannotMove', []));
 	}
 
 	//--------------------------------------------------------------------
 
 	public function testLanguageFileLoading()
 	{
-		$lang = new SecondMockLanguage('en');
+		$this->lang = new SecondMockLanguage('en');
+
+		$this->lang->loadem('More', 'en');
+		$this->assertTrue(in_array('More', $this->lang->loaded()));
 
-		$result = $lang->loadem('More', 'en');
-		$this->assertTrue(in_array('More', $lang->loaded()));
-		$result = $lang->loadem('More', 'en');
-		$this->assertEquals(1, count($lang->loaded())); // should only be there once
+		$this->lang->loadem('More', 'en');
+		$this->assertEquals(1, count($this->lang->loaded())); // should only be there once
 	}
 
 	//--------------------------------------------------------------------
 
 	public function testLanguageFileLoadingReturns()
 	{
-		$lang = new SecondMockLanguage('en');
+		$this->lang = new SecondMockLanguage('en');
 
-		$result = $lang->loadem('More', 'en', true);
-		$this->assertFalse(in_array('More', $lang->loaded()));
+		$result = $this->lang->loadem('More', 'en', true);
+		$this->assertFalse(in_array('More', $this->lang->loaded()));
 		$this->assertEquals(3, count($result));
-		$result = $lang->loadem('More', 'en');
-		$this->assertTrue(in_array('More', $lang->loaded()));
-		$this->assertEquals(1, count($lang->loaded()));
+
+		$result = $this->lang->loadem('More', 'en');
+		$this->assertTrue(in_array('More', $this->lang->loaded()));
+		$this->assertEquals(1, count($this->lang->loaded()));
 	}
 
 	//--------------------------------------------------------------------
 
 	public function testLanguageSameKeyAndFileName()
 	{
-		$lang = new MockLanguage('en');
-
 		// first file data | example.message
-		$lang->setData('example', ['message' => 'This is an example message']);
+		$this->lang->setData('example', ['message' => 'This is an example message']);
 
 		// force loading data into file Example
-		$this->assertEquals('This is an example message', $lang->getLine('example.message'));
+		$this->assertEquals('This is an example message', $this->lang->getLine('example.message'));
 
 		// second file data | another.example
-		$lang->setData('another', ['example' => 'Another example']);
+		$this->lang->setData('another', ['example' => 'Another example']);
 
-		$this->assertEquals('Another example', $lang->getLine('another.example'));
+		$this->assertEquals('Another example', $this->lang->getLine('another.example'));
 	}
 
 	//--------------------------------------------------------------------
 
 	public function testGetLocale()
 	{
-		$language = Services::language('en', false);
-		$this->assertEquals('en', $language->getLocale());
+		$this->lang = Services::language('en', false);
+		$this->assertEquals('en', $this->lang->getLocale());
 	}
 
 	//--------------------------------------------------------------------
@@ -261,7 +265,6 @@ public function MessageBundles()
 	 */
 	public function testBundleUniqueKeys($bundle)
 	{
-		$language = Services::language('en', false);
 		$messages = require SYSTEMPATH . 'Language/en/' . $bundle . '.php';
 		$this->assertGreaterThan(0, count($messages));
 	}
@@ -271,18 +274,18 @@ public function testBundleUniqueKeys($bundle)
 
 	public function testBaseFallbacks()
 	{
-		$language = Services::language('en-ZZ', false);
+		$this->lang = Services::language('en-ZZ', false);
 		// key is in both base and variant; should pick variant
-		$this->assertEquals("It's made of cheese", $language->getLine('More.notaMoon'));
+		$this->assertEquals("It's made of cheese", $this->lang->getLine('More.notaMoon'));
 
 		// key is in base but not variant; should pick base
-		$this->assertEquals('I have a very bad feeling about this', $language->getLine('More.cannotMove'));
+		$this->assertEquals('I have a very bad feeling about this', $this->lang->getLine('More.cannotMove'));
 
 		// key is in variant but not base; should pick variant
-		$this->assertEquals('There is no try', $language->getLine('More.wisdom'));
+		$this->assertEquals('There is no try', $this->lang->getLine('More.wisdom'));
 
 		// key isn't in either base or variant; should return bad key
-		$this->assertEquals('More.shootMe', $language->getLine('More.shootMe'));
+		$this->assertEquals('More.shootMe', $this->lang->getLine('More.shootMe'));
 	}
 
 	//--------------------------------------------------------------------
@@ -301,32 +304,31 @@ public function testBaseFallbacks()
 	 */
 	public function testAllTheWayFallbacks()
 	{
-		$language = Services::language('ab-CD', false);
-		$this->assertEquals('Allin.none', $language->getLine('Allin.none'));
-		$this->assertEquals('Pyramid of Giza', $language->getLine('Allin.one'));
-		$this->assertEquals('gluttony', $language->getLine('Allin.two'));
-		$this->assertEquals('Colossus of Rhodes', $language->getLine('Allin.tre'));
-		$this->assertEquals('four calling birds', $language->getLine('Allin.for'));
-		$this->assertEquals('Temple of Artemis', $language->getLine('Allin.fiv'));
-		$this->assertEquals('envy', $language->getLine('Allin.six'));
-		$this->assertEquals('Hanging Gardens of Babylon', $language->getLine('Allin.sev'));
+		$this->lang = Services::language('ab-CD', false);
+		$this->assertEquals('Allin.none', $this->lang->getLine('Allin.none'));
+		$this->assertEquals('Pyramid of Giza', $this->lang->getLine('Allin.one'));
+		$this->assertEquals('gluttony', $this->lang->getLine('Allin.two'));
+		$this->assertEquals('Colossus of Rhodes', $this->lang->getLine('Allin.tre'));
+		$this->assertEquals('four calling birds', $this->lang->getLine('Allin.for'));
+		$this->assertEquals('Temple of Artemis', $this->lang->getLine('Allin.fiv'));
+		$this->assertEquals('envy', $this->lang->getLine('Allin.six'));
+		$this->assertEquals('Hanging Gardens of Babylon', $this->lang->getLine('Allin.sev'));
 	}
 
 	public function testLanguageNestedArrayDefinition()
 	{
-		$lang = new SecondMockLanguage('en');
-		$lang->loadem('Nested', 'en');
+		$this->lang = new SecondMockLanguage('en');
+		$this->lang->loadem('Nested', 'en');
 
-		$this->assertEquals('e', $lang->getLine('Nested.a.b.c.d'));
+		$this->assertEquals('e', $this->lang->getLine('Nested.a.b.c.d'));
 	}
 
 	public function testLanguageKeySeparatedByDot()
 	{
-		$lang = new SecondMockLanguage('en');
-		$lang->loadem('Foo', 'en');
+		$this->lang = new SecondMockLanguage('en');
+		$this->lang->loadem('Foo', 'en');
 
-		$this->assertEquals('The fieldname field is very short.', $lang->getLine('Foo.bar.min_length1', ['field' => 'fieldname']));
-		$this->assertEquals('The fieldname field is very short.', $lang->getLine('Foo.baz.min_length3.short', ['field' => 'fieldname']));
+		$this->assertEquals('The fieldname field is very short.', $this->lang->getLine('Foo.bar.min_length1', ['field' => 'fieldname']));
+		$this->assertEquals('The fieldname field is very short.', $this->lang->getLine('Foo.baz.min_length3.short', ['field' => 'fieldname']));
 	}
-
 }