Gettext keys.
ICU power.

Source strings as keys, like gettext. ICU formatting for plurals and gender, like Symfony's translator. Plain PHP arrays — no .po, no parsing.

$ composer require cr0w/phrazor

Source patterns
as keys.

The global t() alias is registered at autoload time. If t() is already defined, a notice is issued and you use \phrazor\t() directly. Both signatures are identical.

Simple & substitution
// No vars — returns translation or pattern
t('Welcome');

// Variable substitution — ICU syntax
t('Welcome, {name}', [
    'name' => $name,
]);

// Namespaced form — always available
\phrazor\t('Welcome, {name}', [
    'name' => $name,
]);
Pluralization & gender
// Plural
t('{count, plural,
    =0 {No items}
    one {# item}
    other {# items}
}', ['count' => $n]);

// Gender / select
t('{gender, select,
    male {He liked this}
    female {She liked this}
    other {They liked this}
}', ['gender' => $g]);

Plain PHP arrays.
Nothing to learn.

Each locale file returns a PHP array. The source pattern is the key, the translation is the value. ICU patterns — including multiline pluralization and gender blocks — are valid as both keys and values. If a pattern has no translation, the pattern itself is returned.

i18n/fr_FR.php
return [

    'Welcome' => 'Bienvenue',

    'Welcome, {name}' =>
    'Bienvenue, {name}',

    '{count, plural,
        =0 {No items}
        one {# item}
        other {# items}
    }' =>
    '{count, plural,
        =0 {Aucun élément}
        one {# élément}
        other {# éléments}
    }',

];

en_US requires no locale file. If a pattern has no entry, the pattern itself is the translation. Your source code is already the English copy.

Multiline patterns are supported as keys — phrazor does not normalize or reformat them. Write the pattern however you write it in source and use it as-is in the locale file.

Locale files are plain required PHP — no parsing, no deserialization. The result is cached in memory for the lifetime of the process.

Three constants.
Set them in bootstrap.

Constant Purpose Default
PHRAZOR_I18N_PATH Directory containing locale files. i18n/
PHRAZOR_LOCALE Active locale identifier. en_US
PHRAZOR_MISSING_LOCALE Behaviour when a locale file is not found: error, warn, or silent. warn / silent

Locale resolution order:

argument
PHRAZOR_LOCALE
APP_LOCALE
en_US

When PHRAZOR_MISSING_LOCALE is not set, phrazor warns when APP_DEBUG is truthy and falls back silently in production.

Keep locale files
in sync.

The scanner crawls your source files, extracts every pattern passed to t(), and diffs against your existing locale files — adding new keys, flagging removed ones, leaving existing translations untouched.

CLI
# Scan using phrazor.php in project root
vendor/bin/phrazor scan

# Specify locales
vendor/bin/phrazor scan \
    --locale=fr_FR,de_DE \
    --output=verbose

# Custom function aliases
vendor/bin/phrazor scan \
    --functions=t,_,trans

# Identity mappings for new keys
vendor/bin/phrazor scan --identity
phrazor.php
return [
    'i18n_path' => __DIR__ . '/i18n',
    'source'    => [__DIR__ . '/src'],
    'locales'   => ['fr_FR', 'de_DE'],
    'functions' => ['t', '_'],

    'scan' => [
        'removed'  => 'comment',
        'new_keys' => 'bottom',
        'sort'     => false,
        'identity' => false,
        'output'   => 'summary',
    ],
];

New key placement options:

bottom
Appended after existing keys — default
top
Prepended before existing keys
sort
Entire file sorted alphabetically after update

Uses token_get_all() — not regex. Multiline patterns, heredocs, and nowdocs are extracted correctly. Dynamic patterns such as t($var) or t('a' . $b) are skipped with a warning showing file and line number.

New keys are written as // TODO: comments so translators know what needs attention. Removed keys are commented out rather than deleted, preserving translations if a key returns.