Translation System

The Fuwafuwa Framework includes a powerful internationalization (i18n) system that makes it easy to reach a global audience. With support for multiple languages, flexible parameter formats, automatic pluralization, and a web-based translation editor, you can create applications that feel native to users anywhere in the world.

Internationalization is more than just translating text—it's about creating an experience that respects cultural and linguistic differences. The translation system handles the technical complexities so you can focus on creating great content. From simple string replacement to complex plural forms with named parameters, the system scales to meet your needs.

One of the key design principles is backward compatibility. Existing applications using the older {0} parameter format continue to work without any changes, while new applications can take advantage of the more readable :name format. This gradual migration path means you can adopt new features at your own pace without breaking existing functionality.

✨ Key Features
  • Backward Compatible - Existing {0} format continues to work unchanged
  • Named Parameters - New :name format for better readability and maintainability
  • Pluralization - Automatic singular/plural form selection based on count
  • Fallback Languages - Graceful degradation when translations are missing
  • Performance Caching - Automatic file-based caching for fast translation lookups
  • Web Translation Editor - Browser-based interface for managing translations

Configuration

The translation system is configured through a simple INI file that controls language settings, caching behavior, and feature flags. This central configuration makes it easy to manage translation settings across your application without touching code.

The lang setting specifies the default language for your application, while fallback_lang determines which language to use when a translation is missing. Caching is enabled by default to improve performance, and missing translation logging helps you identify gaps during development.

Add to your configuration file (e.g., app/configs/translation.ini):

[APP]
lang = en
fallback_lang = en
translation_cache_duration = 3600
log_missing_translations = true
missing_translation_format = {{%s:%s}}

[TRANSLATION]
supported_languages = en,id
enable_pluralization = true
enable_json_files = true

Configuration Options Explained

OptionPurpose
langDefault application language code
fallback_langLanguage to use when translation is missing
translation_cache_durationHow long to cache translations (seconds)
log_missing_translationsLog missing translations for development
supported_languagesComma-separated list of available languages
enable_pluralizationEnable automatic plural form selection
enable_json_filesSupport JSON files alongside PHP arrays

File Structure

Translation files are organized by language code and functional area, making it easy to manage large translation sets. Each language has its own directory containing PHP files that return translation arrays. This modular organization allows multiple developers or translators to work on different parts of the translation without conflicts.

The directory structure follows a logical pattern: language code first, then functional groupings like common, auth, register, etc. Nested subdirectories allow for even finer organization when needed. JSON files can coexist with PHP files, providing flexibility for different use cases.

app/i18n/trans/
├── en/
│   ├── common.php
│   ├── auth.php
│   ├── register.php
│   ├── content/
│   │   ├── post.php
│   │   └── common.php
│   └── api.json (optional)
└── id/
    ├── common.php
    ├── auth.php
    ├── register.php
    └── content/
        ├── post.php
        └── common.php

Basic Usage

Using the translation system in your application is straightforward. The t() function retrieves translations by their key, which follows a file.key naming convention. This namespacing keeps translations organized and prevents naming collisions between different parts of your application.

Translations can be used in both controllers (PHP code) and views (templates). In controllers, call the t() function directly. In views, use the template syntax to output translated strings. The system automatically detects the current language and returns the appropriate translation.

Simple Translation

// In controllers
echo t('common.welcome');
echo t('auth.login_success');

// In views
{{ t('common.save') }}
{{ t('user.profile_updated') }}

Parameter Formats

Many translations need to include dynamic values like usernames, dates, or counts. The translation system supports two parameter formats: the legacy {0} positional format for backward compatibility, and the new :name named format for improved readability.

The legacy format works exactly as it always has, requiring no changes to existing code. The new named format makes translations more readable and easier to maintain, especially for complex strings with multiple parameters. Both formats can coexist in the same application, allowing gradual migration.

Legacy Format (Backward Compatible)

// Translation file
'activation_email_sent' => 'Activation email has been sent to {0}',
'user_profile' => 'Profile for {0} ({1})',

// Usage (exactly like before)
echo t('register.activation_email_sent', 'user@example.com');
echo t('example.user_profile', 'john_doe', 'john@example.com');

New Named Format

// Translation file
'hello_user' => 'Hello, :name! Welcome back.',
'user_profile' => 'Profile for :username (:email)',

// Usage with named parameters
echo t('example.hello_user', ['name' => 'John']);
echo t('example.user_profile', [
    'username' => 'john_doe',
    'email' => 'john@example.com'
]);

Named parameters offer several advantages: they're self-documenting, order-independent (useful for languages with different word orders), and less prone to errors when translations are updated. For new projects or when updating existing translations, consider using the named format.

Pluralization New

Handling singular and plural forms correctly is essential for professional localization. Different languages have complex pluralization rules, and hardcoding conditionals makes code messy. The tp() function (translation plural) handles this automatically by selecting the correct form based on the count.

The system works by looking for two translation keys: the base key for singular form, and the base key with _plural suffix for the plural form. The count parameter automatically determines which form to use, and the :count placeholder in the translation is replaced with the actual number.

Use the tp() function for automatic plural forms:

// Translation file setup
return [
    'message' => 'You have :count message',
    'message_plural' => 'You have :count messages',
];

// Usage
echo tp('example.message', 1);     // "You have 1 message"
echo tp('example.message', 5);     // "You have 5 messages"

// With additional parameters
echo tp('example.notification', 3, ['type' => 'email']);

Pluralization isn't just about adding an "s"—many languages have entirely different word forms for different quantities. While the current implementation handles simple singular/plural cases, the foundation is in place for more complex pluralization rules in future versions.

Helper Functions

The translation system provides several helper functions beyond basic t() to handle common scenarios. These helpers make your code more robust and your user experience more polished, especially when dealing with optional features or missing translations.

FunctionDescriptionExample
t($key, ...$args) Basic translation t('common.save')
tp($key, $count, ...$args) Translation with pluralization tp('items.count', 5)
t_exists($key, $lang) Check if translation exists t_exists('common.feature')
t_fallback($key, $fallback, ...$args) Translation with custom fallback t_fallback('key', 'Default')
t_all($section, $lang) Get all translations for a section t_all('common')

Checking Translation Existence

When building features that might not have complete translations, it's good practice to check if a translation exists before using it. The t_exists() function returns true if the translation key is available in the current or fallback language, allowing you to provide alternatives or hide untranslated content gracefully.

if (t_exists('common.advanced_feature')) {
    echo t('common.advanced_feature');
} else {
    echo 'Feature not available';
}

Custom Fallback

Sometimes you need a translation but want to provide a sensible default if it doesn't exist. The t_fallback() function does exactly this—attempt the translation first, then fall back to the provided default text. This is particularly useful for optional features or user-generated content that might not have translations in all languages.

echo t_fallback('optional.feature', 'Default text');
echo t_fallback('user.greeting', 'Hello!', ['name' => 'User']);

JSON File Support

While PHP files are the default format for translations, JSON files provide an alternative that can be more convenient for certain workflows. JSON is particularly useful when translations are managed externally, edited by non-technical translators, or consumed by JavaScript code on the frontend.

JSON files coexist peacefully with PHP translation files, so you can use whichever format makes sense for each part of your application. The translation system automatically detects and loads JSON files when enable_json_files is set to true in your configuration.

You can use JSON files alongside PHP arrays:

// app/i18n/trans/en/api.json
{
    "success": "Operation successful",
    "error": "An error occurred: :message",
    "not_found": "Resource not found"
}

Fallback Language Support

In a perfect world, every translation would be available in every supported language. In reality, translations are often incomplete or lag behind new features. The fallback language system ensures your application remains functional even when translations are missing.

When a translation key isn't found in the current language, the system automatically checks the configured fallback language (typically English, the primary development language). This graceful degradation means users always see appropriate content rather than missing translation keys or error messages.

If a translation is not found in the current language, the system automatically falls back to the configured fallback language:

// If 'new_feature.title' doesn't exist in Indonesian,
// it will automatically use the English version
$f3['APP.lang'] = 'id';
echo t('new_feature.title'); // Falls back to English

Web-Based Translation Editor

Managing translations through text files works well for developers, but can be intimidating for translators and content managers. The web-based translation editor provides a user-friendly interface for managing all translations directly in the browser, making it easier for non-technical team members to contribute to localization efforts.

The editor provides a visual overview of all translation keys, with side-by-side views of different languages. Translators can see which translations are missing, search for specific keys, and make changes without touching any code. Changes are exported back to PHP files, keeping the file-based system as the source of truth.

The framework includes a browser-based translation management interface accessible at /admin/i18n-manager.

Editor Features
  • Visual editing of all translation files
  • Support for nested translation keys
  • Add, edit, and delete translations
  • Search and filter translations
  • Export to PHP files

Caching

Loading translation files on every request would be inefficient, especially for applications with extensive translations. The built-in caching system stores parsed translations in memory, dramatically improving performance while maintaining the simplicity of file-based storage.

The cache automatically invalidates based on the configured duration, or you can clear it manually when translations are updated. The file-based cache means no external services like Redis are required—everything works out of the box with zero configuration.

Translation files are automatically cached to improve performance. Configure cache duration in config.ini:

[APP]
translation_cache_duration = 3600  ; 1 hour in seconds

Missing Translation Logging

During development, it's common to add new features or translation keys before all translations are complete. Missing translation logging helps you identify gaps in your translations by logging every request for a non-existent translation key, along with the requested language.

This feature is invaluable for maintaining translation completeness. By reviewing the logs periodically, you can ensure all user-facing text is properly translated before releases. In production, you may want to disable this logging to reduce noise, or keep it enabled to track missing translations in real-time.

Enable logging to track missing translations during development:

[APP]
log_missing_translations = true
missing_translation_format = {{%s:%s}}  ; Format: {{key:lang}}

Missing translations will be logged:

Missing translation: new_feature.title for language: id

Migration from Old System

If you have an existing application using the older translation system, upgrading is painless. The enhanced system maintains 100% backward compatibility—your existing code continues to work without any modifications. The {0} positional parameter format, the t() function signature, and file structure all remain unchanged.

This backward compatibility means you can adopt new features incrementally. Start by enabling the system in configuration, then gradually update to use named parameters and pluralization where they provide the most value. There's no pressure to do a big-bang migration—evolve at your own pace.

The enhanced system is 100% backward compatible. No changes are required to existing code:

// All existing code continues to work unchanged
echo t('common.save');
echo t('common.record_saved', 'User');
echo t('register.activation_email_sent', $email);

// Optionally upgrade to new features when convenient
echo t('common.record_saved', ['type' => 'User']);
echo t('user.welcome', ['name' => $username, 'email' => $email]);

Best Practices

Following these best practices will help you create a maintainable, professional translation system that scales with your application and provides an excellent experience for international users.

Key Recommendations

1. Maintain compatibility when possible
Keep using {0} format for simple cases where named parameters don't add significant value. This keeps your codebase consistent and easier to understand.
2. Use named parameters for complex translations
Switch to :name format when translations have multiple parameters or when parameter order might vary between languages. The readability benefit is substantial.
3. Use descriptive, hierarchical keys
Choose keys like user.profile.update_success instead of cryptic names like msg1. Hierarchical keys organize translations logically and prevent naming conflicts.
4. Group related translations together
Keep translations for the same feature or module in the same file. This makes it easier for translators to understand context and find related translations.
5. Provide fallbacks for optional content
Use t_fallback() for features that might not have complete translations, ensuring users see appropriate content rather than missing key errors.
6. Test missing translations during development
Enable missing translation logging to identify gaps before releases. Review logs regularly and prioritize translating user-facing content.
7. Implement proper pluralization
Use tp() for any text that refers to counts. Proper pluralization is a hallmark of professional localization and significantly improves user experience.
8. Document context for translators
Add comments in translation files explaining context where needed. What seems obvious to developers may be unclear to translators working with isolated strings.
✨ Translation Workflow Tip

Consider establishing a translation workflow where developers add new keys with English text as the default, then non-technical translators use the web editor to provide translations for other languages. This separation of concerns allows each role to focus on their strengths while maintaining translation quality.

  • Use named parameters - Switch to :name format for complex translations
  • Use descriptive keys - user.profile.update_success instead of msg1
  • Group related translations - Keep related translations in the same file
  • Provide fallbacks - Use t_fallback() for optional features
  • Test missing translations - Enable logging in development
  • Use pluralization - Implement proper plural forms for better UX