Creating Multilingual PHP Apps with Fat-Free Framework

Localization is key for growing any app and reaching a global user base. Learn how to create a multilingual PHP app with Fat-Free Framework.

Background

I recently read a post about an amazing project done with Fat-Free Framework (or simply, “F3”), a PHP micro framework. So I gave it a try to find out how it handles localization.

Getting Started

The good new is: F3 has built-in support for multilingual apps. In this example, I will create a project with the following basic directory structure:

index.php
lib/ (F3 base)
app/ (application files)
    dict/ (create this directory!)
    controllers/
    models/
    views/

F3 automatically detects language files in the ‘dict’ directory. Even better: F3 detects a users language based on the HTTP Accept-Language header coming from the browser and uses the right language file.

Here is my minimal app code in index.php:

<?php
	$f3 = require('lib/base.php');
	$f3->set('LOCALES','app/dict/');
	$f3->route('GET /',
   		function($f3) {
			$template = new Template;
        	echo $template->render('app/views/index.html');
    	}
	);
	$f3->run();
?>

Creating Language Files

For this example I want to use two languages: English and German.

For English, I create a new file (en.php) in the ‘dict’ directory.

<?php
return array(
    'hello' => 'Hello',
    'world' => 'World',
);
?>

For German, I create de.php:

<?php
return array(
    'hello' => 'Hallo',
    'world' => 'Welt',
);
?>

It is also possible to create language files for language variants, such as en-US, en-GB or de-DE, de-AT. If a key doesn’t exist in a variant, F3 will search for it in the root language and at last, always fall back to the default English ‘en.php’ language file.

Using Language Files In F3 Templates

Loading strings from the language files in to your templates is easy, here is my index.html template:

<h1>{{ @hello }}</h1>
<h2>{{ @world }}</h2>

Use Translations In Your Code

I can also access translated strings from within the app code:

$hello = $f3->get('hello');

Advanced Translations

Some of the more advanced localization techniques that F3 supports are placeholders and pluralization.

Placeholders

In the language file:

<?php
return array(
    'hello' => 'Hello',
    'new_messages' => 'You have {0} new messages',
);
?>

In the app code:

echo $f3->format($f3->get('new_messages'), 42);
// Output: "You have 42 new messages"

Pluralization

The plural syntax is a bit tricky, but very helpful. Here is what the language file looks like:

<?php
return array(
    'hello' => 'Hello',
    'orders' => 	'{0, plural,'.
		  'zero	{No new order},'.
		  'one	{One new order},'.
		  'other	{You have # new orders}'.
	'}'
);
?>

Quantities recognized by F3 are ‘zero’, ‘one’, ‘two’ and ‘other’.

Usage in the app code:

echo $f3->format($f3->get('orders'), 0);
// Output: "No new order"

Testing

As mentioned earlier, F3 automatically serves the right language. For testing purposes, if you don’t want to mess around in your browser language settings, you can force F3 to use a specific language file:

// index.php
$f3->set('LOCALES','app/dict/');
$f3->set('LANGUAGE','de');

Managing Translations

Working with .php language files can be cumbersome: new keys must be added to each language file separately, the same goes for updating keys.

Phrase is a translation management tool that addresses some of the issues. It also features a powerful In-Context Editor (Demo), making the process of translating web apps more convenient.

Integrating the Phrase In-Context Editor in your F3 apps is easy. Get our special language file and save it in your ‘dict’ directory. Give it a two letter name that isn’t used, for example ‘xx.php’. In your app, force the use of the Phrase language file that exposes your keys to the Phrase editor:

<?php
    $f3 = require('lib/base.php');
    $f3->set('LOCALES','app/dict/');
    $f3->set('LANGUAGE','xx');
    $f3->route('GET /',
        function($f3) {
            $template = new Template;
            echo $template->render('app/views/index.html');
        }
    );
    $f3->run();
?>

Then include the JavaScript snippet in your templates:

<script>
  var phrase_auth_token = 'YOUR_PHRASEAPP_TOKEN';
  (function() {
    var phraseapp = document.createElement('script'); phraseapp.type = 'text/javascript'; phraseapp.async = true;
    phraseapp.src = ['https://', 'phraseapp.com/assets/phrase/0.1/app.js?', new Date().getTime()].join('');
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(phraseapp, s);
  })();
</script>

That’s it – you can now translate your site using the Phrase In-Context Editor. Phrase also lets you order professional translations. “Se habla Español” for your app? With Phrase, it’s just one click away.

Further Reading

Be sure to subscribe and receive all updates from the Phrase blog straight to your inbox. You’ll receive localization best practices, about cultural aspects of breaking into new markets, guides and tutorials for optimizing software translation and other industry insights and information. Don’t miss out!

Keep exploring

Photo-realistic sheet music featuring developer-style translation code in place of musical notes. The staff lines show snippets like t('auth.signin.button') and JSON structures, combining the aesthetics of musical notation with programming syntax to illustrate the idea of “composable localization.”

Blog post

Localization as code: a composable approach to localization

Why is localization still a manual, disconnected process in a world where everything else is already “as code”? Learn how a composable, developer-friendly approach brings localization into your CI/CD pipeline, with automation, observability, and Git-based workflows built in.

A woman in a light sweater sits in a home office, focused on her laptop, representing a developer or content manager working on WordPress localization tasks in a calm, professional environment.

Blog post

How to build a scalable WordPress i18n workflow

WordPress powers the web, but translating it well takes more than plugins. Discover how to build a scalable localization workflow using gettext, best practices, and the Phrase plugin.

Blog post

Localizing Unity games with the official Phrase plugin

Want to localize your Unity game without the CSV chaos? Discover how the official Phrase Strings Unity plugin simplifies your game’s localization workflow—from string table setup to pulling translations directly into your project. Whether you’re building for German, Serbian, or beyond, this guide shows how to get started fast and localize like a pro.

Blog post

Internationalization beyond code: A developer’s guide to real-world language challenges

Discover how language affects your UI. From text expansion to pluralization, this guide explores key i18n pitfalls and best practices for modern web developers.

A digital artwork featuring the Astro.js logo in bold yellow and purple tones, floating above Earth's horizon with a stunning cosmic nebula in the background. The vibrant space setting symbolizes the global and scalable nature of Astro’s localization capabilities, reinforcing the article’s focus on internationalization in web development.

Blog post

Astro.js localization part 2: dynamic content localization

Learn how to localize your Astro.js website with static and dynamic content translation. Explore Astro’s built-in i18n features and Paraglide for handling UI elements, navigation, and dynamic text seamlessly.