17

I have been building a personal framework of mine which started off as a way to learn the MVC pattern and has progressed now into something that I like more than most frameworks out there (which is probably because I add what I like and change what I don't like but nevertheless) and for good or bad I use it in some projects.

The problem I have now is that I can't come up with a decent way to access my i18n functionality (it's not really i18n it's just translations, doesn't include full i18n support, at least not yet).

The way it works is I use configuration files as language files because I figured it would be pretty convenient to use the Config class to load them since in my framework config files are loaded dynamically - not loaded unless needed, you can grab a glance here

class Config {

    private static $settings = array();

    private function __construct() {
        
    }

    public static function load($file) {
        $path = PROJECT_PATH . '/config/' . $file . '.php';
        if (is_file($path)) {
            $settings = require($path);
        } else {
            throw new Exception('Configuration file [' . $file . '] doesn\'t exist', 500);
        }

        self::$settings[$file] = $settings;
        return true;
    }

    public static function get($file = null) {
        if ($file === null) {
            return self::$settings;
        } elseif (isset(self::$settings[$file]) || self::load($file)) {
            return self::$settings[$file];
        }
    }
}

Where a single configuration file would look something like this

<?php return array(
    'setting0' => 'value',
    'setting1' => 'value',
    ....
);

That allows PHP to cache those files and loading them becomes very fast.

Now on to translations, as I said they are configuration files in a different directory named lang, but I can't just go around calling Config::get('lang/en/myLangFile') every time I need to access a translation, so I invented (invented, huh) the Translations class, that represents a single translations file

class Translations {

    protected $data = [];

    public function __construct(array $translations) {
        $this->data = $translations;
    }

    public function __get($name) {
        return isset($this->data[$name]) ? $this->data[$name] : $name;
    }

}

Now it is super convenient and beautiful to access translations

$t = new Translations([...]);

echo $t->translationKey;

I have a Lang class that is used to set up the user's preferred language among other little things so I figured I would use that as a factory for my Translations classes

class Lang {

    public static function get($file) {
        return new Translations(Config::get('lang/' . self::$lang . '/' . $file));
    }

}

So now all I have to do in order to grab some translations is

$t = Lang::get('myLangFile');

echo $t->translationKey;

In case you are wondering why I have so much static stuff it is because these classes make no sense to be instantiated and I don't like the singleton design pattern, I prefer to have "static classes" even though they are not supported in PHP (yet?).

So far so good, I have got the translations going, but let's get to the problem (finally).

When a view is rendered it is most likely going to print some text out to the user and for that, I need to have a translations object available inside but it is pretty inconvenient to have to pass that from the controller because I would have to go and put that on each method and that would be hell, furthermore, if I do that then some other controller calls the same view without the correct translations objects stuff is going to break, which makes sense but adds complexity to the program.

What I have been doing up until this point is on the top of every view I construct my translations object

<?php $t = Lang::get('myLangFile') ?>

<div><?= $t->helloWorld ?></div>

This works and guarantees me my views will work regardless of who's calling them and basically costs nearly nothing in terms of performance because instantiating a Translations will not copy the array containing the information unless a change is introduced since the code in the constructor is just an assignment, so I guess there's no problem with that but it's just bugging me for some reason that it is not the correct thing to do.

Furthermore, I will need to use a Translations class in a model or validator occasionally, and I would need to instantiate it there too, so in a single execution, I may be instantiating the same Translations object multiple times. To solve this problem I would need to start putting those objects into the registry and I think this would be getting too far.

I would like to see what are some thoughts on this approach as I could be blinded by my own and possibly get some useful advice and stuff. Thanks in advance to anyone who chose to spare their time with my problem!

3 Answers 3

1

<?php $t = Lang::get('myLangFile') ?>

<div><?= $t->helloWorld ?></div>

This works and guarantees me my views will work regardless of who's calling them and basically costs nearly nothing in terms of performance because instantiating a Translations will not copy the array containing the information unless a change is introduced since the code in the constructor is just an assignment, so I guess there's no problem with that but it's just bugging me for some reason that it is not the correct thing to do.

I see what you mean...
I think that one thing that could be bugging you is that by adding into the view the responsibility of loading the language files, you are breaking the Inversion of Control design pattern
Because of that, if for example, your 'myLangFile' gets too big, and you want to split it into 2 separate files, you can't do it without changing the view code.
what do you think?

3
  • 1
    Exactly, that is absolutely a valid reason.
    – php_nub_qq
    Commented Jun 11, 2016 at 11:13
  • 1
    Really? The language file can't include other files in itself? Or read from a database?
    – svidgen
    Commented Jan 7, 2017 at 13:47
  • 1
    @svidgen that is extremely bad design
    – php_nub_qq
    Commented Feb 21, 2017 at 9:08
0

You can inheret from a base class (whch instantiates the Translations), then pass on the translation to your view like you would any other view data.

// translate.php

class translate {
    public function __construct() {
        $t = Lang::get('myLangFile');
    }
}
// controller.php

class controller extends translate {
    echo $t->translationKey;
}

You could even automate the injection of translations into the data variable that gets passed into your views so you don't need to do it manually in every controller).


For controllers that require data validation.

// validate.php

class validate() extends translate {
    // Functions used to construct validation rules in your controller.
}
// controller.php

class controller extends validate {
    // Validate data and populate with translations where needed.
}

I use object inheritence to build out the type of controllers and models I need.

class authenticated {}

class authorised extents authenticated {}

class controller extents authorised {}

...

class controller extents serviceApi {}

class model extends database {}

0

Use a concrete object

I once had to redo a translation system that worked for years but was not meeting current needs. It was instantiated using static methods. It as so much of a nightmare that ultimately it hasn't been possible.

The issue with a static method is that the name of the class is repeated in every file that uses it. For a translator, that is a lot of files, and a project can grow so much that you're not sure anymore you've found them all.

This is avoidable if you use a concrete object, instantiate it only once in the codebase, and the pages receive a translator object that implements an interface.

Then you can stop thinking too hard about your translator implementation. Sure, go ahead with a simple design that works for now: you'll be able to change it when (and if) it doesn't meet your needs anymore. If you implement a simple Translator object that wraps several translators objects you'll be able to even combine different translator implementations, adding whatever new system while keeping the old one running during the transition.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.