4

I am learning PHP (constantly) and I created some time ago a class that handles translations. I want to emulate gettext but getting the translated strings from a database. However, now that I see it again, I don't like that, being a class, to call it I need to use $Translate->text('String_keyword');. I wouldn't like either to have to use $T->a('String_keyword'); since that's completely not intuitive.

I've been thinking a lot on how to make it to be called with a simple _('String_keyword'), gettext style but, from what I've learned from SO, I haven't been able to find a 'great' way to accomplish this. I need to pass somehow the default language to the function, I don't want to pass it every time I call it as it would be _('String_keyword', $User->get('Language'))). I also don't want to include the user-detection script in the _() function, as it only needs to be run once and not every time.

The easiest one would be to use GLOBALS, but I've learned here that they are completely-utterly forbidden (could this be the only case where I can use them?), then I thought to DEFINE a variable with the user's language like define ( USER_LANGUAGE , $User->get('Language') ), but it seems just to be the same as a global. These are the 2 main options I can see, I know there are some other ways like Dependency Injection but they seem to add just too much complication for a so simple request and I haven't yet had time to dig into them.

I was thinking about creating a wrapper first to test it out. Something like this:

function _($Id, $Arg = null)
  {
  $Translate = new Translate (USER_LANGUAGE);
  return $Translate -> text($Id, $Arg)
  }

Here is the translation code. The language is detected before and passed to the object when created.

// Translate text strings
// TO DO: SHOULD, SHOULD change it to PDO! Also, merge the 2 tables into 1
class Translate
  {
  private $Lang;

  function __construct ($Lang)
    {
    $this->Lang = $Lang;
    }

  // Clever. Adds the translation so when codding I don't get annoyed.
  private function add ($Id, $Text)
    {
    $sql="INSERT INTO htranslations (keyword, en, page, last) VALUES ('$Id', '$Text', '".$_SERVER['PHP_SELF']."', now())";

    mysql_query($sql);
    }

  private function retrieve ( $Id )
    {
    $table = is_int ($Id) ? "translations" : "htranslations";      // A small tweak to support the two tables, but they should be merged.
    $results = mysql_query ("SELECT ".mysql_real_escape_string($this->Lang)." FROM ".$table." WHERE keyword='".mysql_real_escape_string($Id)."'");
    $row = mysql_fetch_assoc ($results);
    return mysql_num_rows ($results) ? stripslashes ($row[$this->Lang]) : null;
    }

  // If needed to insert a name, for example, pass it in the $Arg
  public function text($Id, $Arg = null)
    {
    $Text = $this->retrieve($Id);
    if (empty($Text))
      {
      $Text = str_replace("_", " ", $Id);  // If not found, replace all "_" with " " from the input string.
      $this->add($Id, $Text);
      }
    return str_replace("%s", $Arg, $Text);    // Not likely to have more than 2 variables into a single string.
    }
  }

How would you accomplish this in a proper yet simple (for coding) way? Are any of the proposed methods valid or can you come with a better one?

Zoe
  • 27,060
  • 21
  • 118
  • 148
Francisco Presencia
  • 8,732
  • 6
  • 46
  • 90
  • A question in [meta] about what? – Madara's Ghost Nov 04 '12 at 19:33
  • Because I am converting old PHP and mysql code to PHP and, in the future, PDO, but I am still with PHP, so in almost all my question there's still wrong and old mysql code and I don't want that, every time, someone wastes his time saying the same sentence you said. [Here is the question](http://meta.stackexchange.com/questions/153604/not-to-get-extra-advice-in-some-specific-cases). I think that it's only a copy/paste anyway, right? – Francisco Presencia Nov 04 '12 at 19:46
  • 1
    I wasn't implying DON'T USE MYSQL_*!!, I was adding information for you, plus for future visitors of your questions, who may find it useful. If you already know the points given in those links, good for you! But not everyone who may look on this question are :) – Madara's Ghost Nov 04 '12 at 19:49
  • I will make it more obvious as the comments are a bit hidden. Sorry if I sounded rude, didn't mean it at all. – Francisco Presencia Nov 04 '12 at 19:52

2 Answers2

6

If the problem is simply that

$Translate->text('String_keyword');

feels to long, then consider making the Translate object into a Functor by implementing __invoke:

class Translate
{
    // all your PHP code you already have

    public function __invoke($keyword, $Arg = null)
    {
        return $this->text($keyword, $Arg)
    }
}

You can then instantiate the object regularly with all the required dependencies and settings and call it:

$_ = new Translate(/* whatever it needs */);
echo $_('Hallo Welt');

That would not introduce the same amount of coupling and fiddling with the global scope as you currently consider to introduce through a wrapper function or as the Registry/Singleton solution suggested elsewhere. The only drawback is the non-speaking naming of the object variable as $_().

Francisco Presencia
  • 8,732
  • 6
  • 46
  • 90
Gordon
  • 312,688
  • 75
  • 539
  • 559
1

I would use a registry or make Translate a singleton. When i first initalize it i would pass in the language which would be dont in the bootstrap phase of the request. Then i would add methods to change the language later if necessary.

After doing that your function becomes pretty simple:

// singleton version
function _($id, $arg = null) {
   return Translate::getInstance()->text($id, $arg);
}


// registry version
function _($id, $arg = null) {
  return Registry::get('Translate')->text($id, $arg);
}

And then in your bootstap phase you would do something like:

$lang = get_user_lang(); // replace with however you do this

//registry version
Registry::set('Tranlaste', new Translate($lang));

// or the singleton version
// youd use create instance instead of getInstance 
// so you can manage the case where you try to call 
// getInstance before a language is set
Translate::createInstance($lang);
prodigitalson
  • 60,050
  • 10
  • 100
  • 114
  • Needless to say, this is effectively the same as using Globals. If the OP is really concerned about not using Globals, this is not a solution. – Gordon Nov 04 '12 at 19:20
  • @prodiginalson The singleton bit should not make it a function but for the wrapper it seems slick. I surely won't need to change the language later. I didn't think about it but it seems perfect. I have no idea about registries, I will check them, know any good page? – Francisco Presencia Nov 04 '12 at 19:21
  • @Gordon, Is it? wouldn't making the $User class a singleton and then calling it inside _() be the actual equivalent as a global, but this something different? – Francisco Presencia Nov 04 '12 at 19:23
  • 1
    @FrankPresenciaFandos Registries and Singletons are Global State. Also Registries violate Law of Demeter. – Gordon Nov 04 '12 at 19:25
  • Check out the [PoEAA description](http://martinfowler.com/eaaCatalog/registry.html) – prodigitalson Nov 04 '12 at 19:26
  • @gordon: Well the proper thing to do would be set up the intance at bootstrap and then inject it down the chain for where it will be needed, but without better knowledge of how the application is written one cant give advice on that, Thus the Registry/Singleton which is better than just using free floating global IMO. – prodigitalson Nov 04 '12 at 19:28
  • 1
    @prodigitalson It's the same as a free floating global if you ask me. On a sidenote, you dont inject "down the chain". you ["assemble inside out"](http://stackoverflow.com/questions/6094744/dependency-hell-how-does-one-pass-dependencies-to-deeply-nested-objects/6095002#6095002) ;) – Gordon Nov 04 '12 at 19:32
  • @Gordon: So then where do you get the dependency from? Whether its officially a singleton or not there are many instances where i only ever want a single instance of x used during a request across the entire application? Normally i use some kind of service provider for this, but then I still have to inject the service instance or what have you. – prodigitalson Nov 04 '12 at 19:40
  • @prodigitalson A plain old factory or builder as shown in the link above will do fine. Also, keep in mind that the point of a Singleton is to ensure singularity and global access. Most of the time devs only need a single instance, which can easily be achieved by simply creating just that one instance. As long as creating a second instance does not kill kittens, you don't need to enforce singularity. See http://butunclebob.com/ArticleS.UncleBob.SingletonVsJustCreateOne – Gordon Nov 04 '12 at 22:52