I am starting to add multi-language to a site that uses Laravel for frontend.
However, due to lack of resources at the moment, I am translating the strings as I go. It will be a while before whole site is translated.
The only challenge is that if a text is not translated what get displayed is the key. I would like to specify a default/fallback string for such.
e.g.
{{ trans('site.'.$name) }}
If I pass 'Business' as $name and there' no translation for 'Business' in site.php lang file I end up with site.Business on the frontend. This messes up everything. In the worst case, if there's no site.Business, Laravel should output Business.
Even better it should provide an option for default/fallback string.
Is this possible?
On a side note is there a free translation for common words? This will save time having to translate everything myself.
Thanks.
The fallback language is what you should be using. See the docs
You may also configure a "fallback language", which will be used when
the active language does not contain a given language line. Like the
default language, the fallback language is also configured in the
app/config/app.php configuration file:
'fallback_locale' => 'en',
It will surely take you just as much time writing in a fallback inline as it would simply writing in the fallback in a parallel translation file as you write in the translation key. The time spent thinking of an alternate way versus just doing it is going to be negligible in the end.
If you really want an inline fallback, then you need to create a new helper method that does something different. So prepare yourself for some home brewed awesomeness.
Let's create a new function that we can use in any view. I will use the method described by Joseph Sibler. Create a file called helpers.php inside app. Then add this to your composer.json in the autoload object under a files array as "app/helpers.php". Not sure what I mean? See his answer. After adding, run composer dump-autoload.
Now, let's add a trans_fb() method that will take all the parameters of the trans() method, but with a fallback as well. I will define this method such that the first two arguments are required, (the key, and the fallback).
If Laravel cannot find the translation key (it searches in resources/lang/en/auth.php for example auth.failed as a key) it will use the fallback instead, and pass any other optional arguments for the original method.
<?php
if (! function_exists('trans_fb')) {
/**
* Translate the given message with a fallback string if none exists.
*
* #param string $id
* #param string $fallback
* #param array $parameters
* #param string $domain
* #param string $locale
* #return \Symfony\Component\Translation\TranslatorInterface|string
*/
function trans_fb($id, $fallback, $parameters = [], $domain = 'messages', $locale = null)
{
return ($id === ($translation = trans($id, $parameters, $domain, $locale))) ? $fallback : $translation;
}
}
You can then use this in a template like so:
{{ trans_fb("i.love.laravel", "I love Laravel!") }}
I came up with this solution which uses the same parameters as trans() method and when key is not translated, it returns the translated string using the application 'fallback_locale' from app.php.
I still wonder why it's not implemented natively in Laravel and Laravel just returnes untranslated key.
if (!function_exists('trans_fb')) {
function trans_fb(string $key, array $replace = [], ?string $locale = null)
{
$translation = trans($key, $replace, $locale);
if ($key === $translation) {
return trans($key, $replace, config('app.fallback_locale'));
}
return $translation;
}
}
Related
So I just started web development on my new job, meaning I only have very basic knowledge of php, my html, css and js knowledge is a lot better.
I'm trying to build a basic multiple language site via php sessions but I just can't get it to work the way I want.
I have an index.php file and two more folders called EN and DE in which I have the Englisch.html and the Deutsch.html file. The html files only contain some text and it's my goal to have two buttons (or similar) at the top of my site called EN and DE which switch the session to include the files in said folders and display the text beneeth the buttons and keep the language switch function on every page if I had more then one file per language. By default I want the language to be english so load the Englisch.html first.
I'm guessing I need to create an if else statement on every html file that checks the session so something like: if session EN include Englisch.html elseif session DE include Deutsch.html, and on the index.php I somehow need to set the parameters for the session so something like: startSession EN include Englisch.html startSession DE include Deutsch.html ?
I have no idea how far off I am and any help, espacially actual code examples would be greatly appreciated. I hope this described my problem precisely enough.
Your attempted solution is going to bite you in the long run.
It may seem like an easy solution to switch between different files for different languages, yet assume that your website becomes more dynamic, instead of *.html files you want to deal with *.php files, and then would need to have the same logic inside each of your localized files. It doesn't scale well.
I recommend using a translation libary, there are many available, I had good success with symfony's translation, that you can also include in any php project.
Then translation becomes:
$translatedString = $translator->trans("My content");
And the translation can then be maintained in yaml files and depending on the locale, the right language is chosen and each untranslated string will default to English.
And now, whenever your logic changes it is just at one place where you need to adapt it.
I agree with K0pernikus in that your solution won't scale well. You can write a simple translation library yourself in under an hour and you can then test it to see if it will be robust enough for your needs.
The idea is to have language files without any logic in them. You simply call the file you need. All the logic about which language, file, translation key etc. is contained in your library.
I'll keep it very simple and down to a single file; of course, the actual translations will each need to be stored in a file too.
Directory structure:
/locale/en/messages.php
/locale/fr/messages.php
translator.php (this is the main library file and needs to be included on every page)
Within each messages.php file you need to return an array of translation keys and their respective translation. The translation keys are what you will use in your views. These files will become large with many hundreds or thousands of lines for larger applications. If you intend to keep this home-grown solution you'll need to implement caching. That being said, I have files with hundreds of translations and don't notice any significant performance hit.
<?php
// en
return array(
'applicationName' => 'Matt\'s Marvelous Mysteries',
...
<?php
// fr
return array(
'applicationName' => 'Les merveilleux mystères de Matt',
...
Next, you need a library to read these translations and return the actual translation text to you. This file could simply be a collection of helper functions or a full-blown OOP system.
For simplicity, here is a collection of helper methods that get the job done. It does not implement parameterized substitution and you can add that feature relatively easily by making t() accept a 2nd argument, but that is a whole different topic for another time.
The main method in here is t(). It is very simple and accepts a single translation key. Ex. applicationName or greeting.
Firstly, it tries to determine which language to use. It does this in a sequence of priority: URL, session, browser, fallback.
It first tries to get the language/locale from the URL by looking for
a query string parameter named lang. If you think about it, that makes sense because a user intends to switch their language by clicking a
link that says "English" or "French".
If it doesn't find one in the URL it then moves on to check for one
in the session. Again, if it finds it there it uses it.
If it finds it neither in the URL nor the session, then it checks the
browser/headers from the request.
Finally, if it isn't found in any of those 3 locations then it falls
back to the default language as specified in $defaultLanguage.
Once a language has been found, it puts it into the session so the next request doesn't need to go through all that again. It also loads the appropriate messages.php file based on the discovered language.
Finally, once the language has been found and right file has been loaded into memory it searches for the given $key and returns the appropriate translation. If the $key is not found then it simply returns the given $key which will show up in your views so you know something went horribly wrong and you need to start debugging.
<?php
/**
* Performs the actual translation based on the given key. This is the method that is used
* in the actual views to translate a message.
*
* #param $key
* #return mixed
* #throws Exception
*/
function t($key)
{
$language = getLanguage();
$messages = require "{$_SERVER['DOCUMENT_ROOT']}/locale/{$language}/messages.php";
return (array_key_exists($key, $messages))
? $messages[$key]
: $key;
}
/**
* Returns the language as defined by either the URL, session, or browser setting.
* If a language could not be determined, or is not in a list of supported languages, the default
* language passed in to this method will be returned.
*
* #param string $defaultLanguage
* #return string
*/
function getLanguage($defaultLanguage = 'en')
{
$language = null;
if (isset($_GET['lang'])) {
$language = $_GET['lang'];
} elseif (isset($_SESSION['LANG'])) {
$language = $_SESSION['LANG'];
} else {
$language = getLanguageFromBrowser($defaultLanguage);
}
// If the language given to us is not in our list of supported languages, use the default language.
if (!isset($language) || !in_array($language, getSupportedLanguages())) {
$language = $defaultLanguage;
}
// Store the current language to the session for future use.
$_SESSION['LANG'] = $language;
return $language;
}
/**
* Returns the language that the client's browser is set to use. If we're unable to
* determine a language from the browser this will return the default language passed
* in.
*
* #param string $defaultLanguage
* #return int|string
*/
function getLanguageFromBrowser($defaultLanguage = 'en')
{
$languages = [];
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
// break up string into pieces (languages and q factors)
preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $lang_parse);
if (count($lang_parse[1])) {
// create a list like "en" => 0.8
$languages = array_combine($lang_parse[1], $lang_parse[4]);
// set default to 1 for any without q factor
foreach ($languages as $lang => $val) {
if ($val === '') $languages[$lang] = 1;
}
// sort list based on value
arsort($languages, SORT_NUMERIC);
}
}
$supportedLanguages = getSupportedLanguages();
foreach ($languages as $locale => $weighting) {
// We're dealing with locale: Ex. en-US
if (preg_match("/[a-z]{2}-[A-Z]{2}/", $locale)) {
$browserLanguage = substr($locale, 0, 2);
} else {
// Probably dealing with a language: Ex. en
$browserLanguage = $locale;
}
if (in_array($browserLanguage, $supportedLanguages)) {
return $browserLanguage;
}
}
return $defaultLanguage;
}
/**
* Returns an array of languages this web application supports.
*
* #return array
*/
function getSupportedLanguages()
{
return [
'en',
'fr'
];
}
To use it, save these methods into a file called translator.php and then include that file in every page you want to use translations.
Sample:
<?php
session_start();
require_once('translator.php');
// Output your language switcheroo-gadget
if (getLanguage() === 'en') {
echo 'French';
} else {
echo 'English';
}
// Your code... blah blah
// Ahh.. Finally, a translation!
echo '<h1>' . t('applicationName') . '</h1>';
Edit
The last thing I will say is that there is a whole world out there of localization, internationalization (often abbreviated as i18n) which you can learn about.
In my example, I simplistically called it language but often people referer to it as locale but that has a different meaning and syntax. For example, there is a difference between en_CA and en_US and en_GB; all of which are English but have regional differences.
echo t('salutation');
I want to add some HTML to Symfony 2 Translation. This way I will know which phrases in my application are translated and which not.
I found in "Symfony\Component\Translation\Translator.php" function "trans". Now i want to add something in function return, for example "< /br>":
/**
* {#inheritdoc}
*
* #api
*/
public function trans($id, array $parameters = array(), $domain = null, $locale = null)
{
if (null === $locale) {
$locale = $this->getLocale();
} else {
$this->assertValidLocale($locale);
}
if (null === $domain) {
$domain = 'messages';
}
if (!isset($this->catalogues[$locale])) {
$this->loadCatalogue($locale);
}
return strtr($this->catalogues[$locale]->get((string) $id, $domain)."</br>", $parameters);
}
The issue is that when I run my application I'm getting for example "Tag< / b r>" (I have add spaces because in normal way it doesn't show here. HTML doesn't interprate this as HTML code but as a string. Is there any way to achieve what I want ? Maybe it is but in the other way ?
This happens because you have the Twig Escaper extension active. That extension adds automatic output escaping to Twig, it defines the autoescape tag and the raw filter.
So I think the best option you got here is to define a new twig extension to let you translate your html strings without having to repeat myvar|raw each time.
To see how it is possible to create a new Twig extension please check the docs here.
Use the same extension when escaping for JS and there should be no need to use anything else especially in your PHP controllers. That's because the escaping is done at the Twig level. Just remember to declare your new Twig filter as safe to avoid automatic escaping again:
$filter = new Twig_SimpleFilter('nl2br', 'nl2br', array('is_safe' => array('html')));
If you need to do some extra processing with the requested data so that you can track what strings are being requested and what not then just declare a new service as a proxy to the Symfony translation one. Your Twig extension can use the same service. This way you can converge all the requests to one single service.
Here a few useful links for you:
http://twig.sensiolabs.org/doc/api.html#escaper-extension
http://twig.sensiolabs.org/doc/advanced.html#automatic-escaping
I'll suggest you simply to use Markdown in your translation.
Then you can parse your translated message with a Markdown parser.
Example in Twig: 'my.message'|trans|markdown (I suppose you have a Markdown filter, there is KnpMarkdownBundle)
MY SOLUTION:
I have achieved what I wanted in few steps:
Override Translator.php and change translator.class in parameters.yml
public function trans($id, $parameters, $domain, $locale)
{
$return = parent::trans($id, $parameters, $domain, $locale);
return "".$return.'');
}
Set translation class in your css.
Set autoescape option to false in parameters.yml
twig:
autoescape: false
I trying to test my multilanguage app. I have four languages form in my application. I try to test indexAction(), when crawler go through my page I want to check count of title, but title can be in english or in japanese for example. When I pass translation key it does not work. Here is code:
$this->assertEquals(1, $crawler->filter('html:contains("logo_text")')->count());
So the question is, can I pass translation key into tests? Or I need somehow hardcode value?
You can try this solution by Florian Eckerstorfer:
https://florian.ec/articles/use-translation-keys-in-symfony2-functional-tests/
It basically creates a new translator that will return a key instead of a real translation.
class NoTranslator implements TranslatorInterface
{
public function trans($id, array $parameters = array(), $domain = null, $locale = null)
{
return $id;
}
...
}
And than registering it:
# app/config/config_test.yml
parameters:
translator.class: Acme\DemoBundle\Translation\Translator\NoTranslator
The blog post also describes possibility of using Compiler Passes. A lot more complex solution so you can start with the one above.
Started Googling today to research implementing Zend_Translate in a Zend 1.6.x project i have recently been assigned to. But i am finding it difficult to get to usable/appropriate sources of information.
Implemented simple Array adapter, which works nicely.
Basic overlay of the implementation as follows:
in the Language file:
return array(
'testKey' => 'Hello World!');
in SomeController.php: (added translate to the registry)
public function init()
{
...
$this->_translate = Zend_Registry::get('translate');
...
}
in the view:
echo $translate->_('testKey');
I would like to know if it is possible to retrieve more than just one element from the language array? Something like:
$phraseList= $translate->_('lanKey1','lanKey1'..'n');
//or
$phraseList= $translate->_( array('lanKey1','lanKey1'..'n') );
Or at the least does anyone have resources to point out, or a direction to research in?
Many thanks,
David
No, you can pass one item at a time.
You can refer the source code. Its a better resource than a documentation.
/**
* Translates the given string
* returns the translation
*
* #param string $messageId Translation string
* #param string|Zend_Locale $locale (optional) Locale/Language to use, identical with locale
* identifier, #see Zend_Locale for more information
* #return string
*/
public function _($messageId, $locale = null)
{
return $this->translate($messageId, $locale);
}
FYI: Zend_Translate_Adapter
Using the Zend Framework and the url method for the view:
$this->url(array('field1' => this, 'field2' => 'is', 'field3' => 'my example'), 'route_name');
Where route_name is the name of the url route and each field# is retrieved from the database.
I noticed that by default it changes spaces in Controller/Action names into plus sign so that what looked like:
www.example.com/this is my example
to
www.example.com/this+is+my+example
I would like to change the separatoer from + to - to have something like
www.example.com/this-is-my-example
I know that another thread: How to change the separation character of Zend Url?
as documented a way to do it which I tried without success.
A thorough explanation on how to do it would be much appreciated.
EDIT2: I know where the problem lies if anyone is interested, it comes from the way the url is assemble, it uses urlencode which converts all non-alphanumeric characters expect - and _ and the spaces as +, there is no way to override that than replace the character create the url manually (as Maxime suggested) or create a custom url function replacing the characters (as suggest by aporat)...
Thanks!
If you really want to do that, you can extend the stock Zend_View_Helper_Url view helper and add your url logic into your view helper.
<?php
namespace Application\View\Helper;
class MyUrl extends \Zend_View_Helper_Url
{
/**
* Generates an url given the name of a route.
*
* #access public
*
* #param array $urlOptions Options passed to the assemble method of the Route object.
* #param mixed $name The name of a Route to use. If null it will use the current Route
* #param bool $reset Whether or not to reset the route defaults with those provided
* #return string Url for the link href attribute.
*/
public function myUrl(array $urlOptions = array(), $name = null, $reset = false, $encode = true)
{
return str_replace('+', '-', parent::url($urlOptions, $name, $reset, $encode));
}
}
and then just load your new view helper and you're good to go:
$helper = new \Application\View\Helper\MyUrl;
$this->view->registerHelper($helper, 'myUrl');
Unfortunately, you can't set anything before calling the url(...) function to achieve what you want to do. The reason is that when the URL is assembled, it uses the php urlencode(...) function.
That said, you still have many options:
1) You simply don't use the url(...) function and create your URLs manually. (Best option)
2) You create a new helper that acts like url(...) but add extra changes to the function to achieve what you want to do.
3) You take the output of the url(...) function and do a str_replace to change + with -. (I DO NOT recommend that option)
Personally, I create all my URLs manually to avoid this kind of problem.