afe/translation-tool-bundle
Symfony bundle providing CLI tools to audit translation keys: detect duplicated translation codes and find unused keys by scanning Twig/HTML/PHP/JS sources and YAML translation files. Configurable include/exclude paths, locales, and file masks.
Installation:
composer require afe/translation-tool-bundle:dev-master
Register the bundle in AppKernel.php:
new Afe\TranslationToolBundle\AfeTranslationToolBundle(),
Configure:
Add minimal config in config/packages/afe_translation_tool.yaml (or config.yml):
afe_translation_tool:
translation_files_dir_path: "%kernel.root_dir%/../translations"
translation_files_locale: "en"
src_dir_path: "%kernel.root_dir%/../src"
First Use Case: Run the duplicate check command:
php bin/console afe:translation:check:codes
This will scan your translation files for duplicate keys and display results.
Translation Maintenance Workflow:
afe:translation:check:codes and afe:translation:check:unused before merging PRs to catch issues early.Integration with CI: Add commands to your CI pipeline (e.g., GitHub Actions) to fail builds if duplicates/unused keys exceed thresholds:
- name: Check translations
run: |
php bin/console afe:translation:check:codes --format=json | jq '.duplicates | length' -r > duplicates_count.txt
if [ $(cat duplicates_count.txt) -gt 0 ]; then exit 1; fi
Local Development:
--format=json flag for programmatic access:
php bin/console afe:translation:check:unused --format=json > unused_keys.json
config/packages/afe_translation_tool.yaml.translation_files_dir_path to target non-standard paths (e.g., ../custom/translations).translation_files_locale in config or via environment variables.excluded_translation_file_mask to ignore legacy or auto-generated files (e.g., ["messages.*.yml"]).Performance:
vendor and node_modules to improve speed.excluded_directories: ["vendor", "node_modules", "var/cache"]
False Positives:
{{ key }} in Twig) may trigger "unused" warnings.excluded_file_mask: ["*.twig", "*.js"]
YAML-Only Support:
.yml files are processed. JSON/other formats require manual checks.excluded_translation_file_mask to skip unsupported formats.Case Sensitivity:
home.welcome and HOME.WELCOME are treated as distinct.snake_case) in your team’s style guide.Command Output:
grep/jq for better readability:
php bin/console afe:translation:check:unused | grep -E "unused|warning"
--verbose to debug file paths:
php bin/console afe:translation:check:codes --verbose
php bin/console afe:translation:check:unused > unused_report.txt
Custom Validators: Extend the bundle by creating a custom command to validate keys against a schema (e.g., regex patterns):
// src/Command/ValidateTranslationKeysCommand.php
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ValidateTranslationKeysCommand extends Command {
protected function execute(InputInterface $input, OutputInterface $output) {
$keys = $this->getTranslationKeys(); // Implement logic to fetch keys
foreach ($keys as $key) {
if (!preg_match('/^[a-z_]+\.[a-z_]+$/', $key)) {
$output->writeln("<error>Invalid key: $key</error>");
}
}
}
}
Post-Processing: Automate fixes by parsing JSON output and updating files:
$unusedKeys = json_decode(file_get_contents('unused_keys.json'), true);
foreach ($unusedKeys['unused'] as $file => $keys) {
$translationFile = sprintf('%s/translations/%s/%s.yml', $rootDir, $locale, $file);
$translations = yaml_parse(file_get_contents($translationFile));
foreach ($keys as $key) {
unset($translations[$key]);
}
file_put_contents($translationFile, yaml_emit($translations));
}
Event Listeners:
Trigger checks on file changes (e.g., via FileSystemEvents):
// src/EventListener/TranslationListener.php
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\Event\KernelEvent;
class TranslationListener {
public function onKernelRequest(KernelEvent $event) {
if ($event->isMainRequest()) {
$fs = new Filesystem();
if ($fs->exists('translations/fr/messages.yml')) {
$this->runTranslationChecks();
}
}
}
}
How can I help you explore Laravel packages today?