From 2052e9cedfff011e2418567836983b57c805c952 Mon Sep 17 00:00:00 2001 From: Holger Schmermbeck Date: Sun, 2 Nov 2025 21:25:01 +0100 Subject: [PATCH 1/3] feat: add Translation.io configuration and setup - Install tio/laravel package for Translation.io integration - Create config/translation.php with environment-based API key - Add TRANSLATIONIO_KEY to .env.example as placeholder - Initialize Translation.io sync (php artisan translation:init) - Add Translation.io credit to README (Open Source plan requirement) - Update CHANGELOG with i18n additions This commit implements the configuration and setup for multi-language support (English and German) using Translation.io. The actual middleware implementation and string translations will follow in a separate PR. Fixes #84 Part of: #83 --- CHANGELOG.md | 4 + README.md | 4 + composer.json | 3 +- composer.lock | 224 ++++++++++++++++++++++++++++++++++++++++- config/translation.php | 51 ++++++++++ 5 files changed, 284 insertions(+), 2 deletions(-) create mode 100644 config/translation.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d3155d..3e28f53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Translation.io integration for multi-language support (en, de) +- Configuration file `config/translation.php` for Translation.io +- `TRANSLATIONIO_KEY` environment variable for API key management +- Translation management via `php artisan translation:*` commands - Pint `--test --dirty` workflow in preflight script for CI parity - Pre-commit hook for Laravel Pint auto-formatting - CHANGELOG validation in preflight script diff --git a/README.md b/README.md index 7c5e257..5a4df9d 100644 --- a/README.md +++ b/README.md @@ -371,6 +371,10 @@ This project adheres to the [Contributor Covenant Code of Conduct](CODE_OF_CONDU - **Discussions:** [GitHub Discussions](https://github.com/orgs/SecPal/discussions) - **Documentation:** [Project Wiki](https://github.com/SecPal/api/wiki) +## Translation + +SecPal uses [Translation.io](https://translation.io) for managing translations. Translation.io offers free unlimited accounts for open source projects. + ## Related Repositories - [SecPal/.github](https://github.com/SecPal/.github) - Organization-wide settings diff --git a/composer.json b/composer.json index 9e27d72..7b5b3af 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "laravel/framework": "^12.36", "laravel/sanctum": "^4.2", "laravel/tinker": "^2.10.1", - "spatie/laravel-permission": "^6.22" + "spatie/laravel-permission": "^6.22", + "tio/laravel": "^1.23" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index d7c0c2d..5b81977 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8d74c4e4f05036508492120903d1e85f", + "content-hash": "3fa0f6de32db46dea407064d0e6b9546", "packages": [ { "name": "brick/math", @@ -580,6 +580,152 @@ ], "time": "2023-10-12T05:21:21+00:00" }, + { + "name": "gettext/gettext", + "version": "v4.8.12", + "source": { + "type": "git", + "url": "https://github.com/php-gettext/Gettext.git", + "reference": "11af89ee6c087db3cf09ce2111a150bca5c46e12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/11af89ee6c087db3cf09ce2111a150bca5c46e12", + "reference": "11af89ee6c087db3cf09ce2111a150bca5c46e12", + "shasum": "" + }, + "require": { + "gettext/languages": "^2.3", + "php": ">=5.4.0" + }, + "require-dev": { + "illuminate/view": "^5.0.x-dev", + "phpunit/phpunit": "^4.8|^5.7|^6.5", + "squizlabs/php_codesniffer": "^3.0", + "symfony/yaml": "~2", + "twig/extensions": "*", + "twig/twig": "^1.31|^2.0" + }, + "suggest": { + "illuminate/view": "Is necessary if you want to use the Blade extractor", + "symfony/yaml": "Is necessary if you want to use the Yaml extractor/generator", + "twig/extensions": "Is necessary if you want to use the Twig extractor", + "twig/twig": "Is necessary if you want to use the Twig extractor" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gettext\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oscar Otero", + "email": "oom@oscarotero.com", + "homepage": "http://oscarotero.com", + "role": "Developer" + } + ], + "description": "PHP gettext manager", + "homepage": "https://github.com/oscarotero/Gettext", + "keywords": [ + "JS", + "gettext", + "i18n", + "mo", + "po", + "translation" + ], + "support": { + "email": "oom@oscarotero.com", + "issues": "https://github.com/oscarotero/Gettext/issues", + "source": "https://github.com/php-gettext/Gettext/tree/v4.8.12" + }, + "funding": [ + { + "url": "https://paypal.me/oscarotero", + "type": "custom" + }, + { + "url": "https://github.com/oscarotero", + "type": "github" + }, + { + "url": "https://www.patreon.com/misteroom", + "type": "patreon" + } + ], + "time": "2024-05-18T10:25:07+00:00" + }, + { + "name": "gettext/languages", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/php-gettext/Languages.git", + "reference": "38ea0482f649e0802e475f0ed19fa993bcb7a618" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-gettext/Languages/zipball/38ea0482f649e0802e475f0ed19fa993bcb7a618", + "reference": "38ea0482f649e0802e475f0ed19fa993bcb7a618", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16.0", + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4" + }, + "bin": [ + "bin/export-plural-rules" + ], + "type": "library", + "autoload": { + "psr-4": { + "Gettext\\Languages\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michele Locati", + "email": "mlocati@gmail.com", + "role": "Developer" + } + ], + "description": "gettext languages with plural rules", + "homepage": "https://github.com/php-gettext/Languages", + "keywords": [ + "cldr", + "i18n", + "internationalization", + "l10n", + "language", + "languages", + "localization", + "php", + "plural", + "plural rules", + "plurals", + "translate", + "translations", + "unicode" + ], + "support": { + "issues": "https://github.com/php-gettext/Languages/issues", + "source": "https://github.com/php-gettext/Languages/tree/2.6.0" + }, + "time": "2019-11-13T10:30:21+00:00" + }, { "name": "graham-campbell/result-type", "version": "v1.1.3", @@ -5957,6 +6103,82 @@ }, "time": "2024-12-21T16:25:41+00:00" }, + { + "name": "tio/laravel", + "version": "v1.23", + "source": { + "type": "git", + "url": "https://github.com/translation/laravel.git", + "reference": "bbcf9ae4c48abb6737040e908f6a03d6c5e42b5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/translation/laravel/zipball/bbcf9ae4c48abb6737040e908f6a03d6c5e42b5c", + "reference": "bbcf9ae4c48abb6737040e908f6a03d6c5e42b5c", + "shasum": "" + }, + "require": { + "gettext/gettext": "^4.8.4", + "gettext/languages": "<=2.6.0", + "guzzlehttp/guzzle": ">=6.3", + "illuminate/config": ">=5.5", + "illuminate/console": ">=5.5", + "illuminate/filesystem": ">=5.5", + "illuminate/support": ">=5.5", + "illuminate/translation": ">=5.5", + "php": ">=7.0" + }, + "require-dev": { + "orchestra/testbench": ">=3.1", + "php-vcr/php-vcr": "^1.3", + "phpunit/phpunit": ">=5.7.27", + "squizlabs/php_codesniffer": "~3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Tio\\Laravel\\ServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Helpers/translator_functions.php" + ], + "psr-4": { + "Tio\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Armands Leinieks", + "email": "armands.leinieks@gmail.com" + }, + { + "name": "Michaƫl Hoste", + "email": "michael.hoste@gmail.com" + } + ], + "description": "Add this package to localize your Laravel application (PHP, JSON or GetText).", + "homepage": "https://github.com/translation/laravel", + "keywords": [ + "gettext", + "laravel", + "localization", + "translation", + "translation.io" + ], + "support": { + "issues": "https://github.com/translation/laravel/issues", + "source": "https://github.com/translation/laravel/tree/v1.23" + }, + "time": "2023-04-13T09:53:38+00:00" + }, { "name": "vlucas/phpdotenv", "version": "v5.6.2", diff --git a/config/translation.php b/config/translation.php new file mode 100644 index 0000000..7625688 --- /dev/null +++ b/config/translation.php @@ -0,0 +1,51 @@ + env('TRANSLATIONIO_KEY'), + + /* + |-------------------------------------------------------------------------- + | Source Locale + |-------------------------------------------------------------------------- + | + | The source locale is the language in which you write your application. + | All translations will be created from this base language. + | + */ + 'source_locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Target Locales + |-------------------------------------------------------------------------- + | + | The target locales are the languages you want your application to be + | translated into. Add or remove locales as needed. + | + */ + 'target_locales' => ['de'], + + /* + |-------------------------------------------------------------------------- + | Gettext Parse Paths + |-------------------------------------------------------------------------- + | + | Directories to scan for Gettext strings. Translation.io will scan these + | paths to extract translatable strings from your application code. + | + */ + 'gettext_parse_paths' => ['app', 'resources'], +]; From 66a2b7127ddaf0fb032ddd26b6e5966d6814663b Mon Sep 17 00:00:00 2001 From: Holger Schmermbeck Date: Sun, 2 Nov 2025 21:36:53 +0100 Subject: [PATCH 2/3] test: add Translation.io config tests and fix missing .env.example - Add comprehensive PEST tests for translation.php configuration - Test config structure, required keys, and data types - Test source locale (en) and target locales (de) - Test gettext parse paths exist as valid directories - Verify API key loading from environment variable - Add missing .env.example changes to commit Addresses Copilot review comments on PR #88: 1. Added test coverage for configuration (PEST format) 2. Ensured .env.example is properly committed All 8 new tests passing (24 assertions total). Note: Tests are written in PEST format as per project guidelines, NOT PHPUnit. This follows the TDD and quality-first principles. --- .env.example | 80 ++++++++++++++++++++++++ tests/Feature/TranslationConfigTest.php | 82 +++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 .env.example create mode 100644 tests/Feature/TranslationConfigTest.php diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5edc633 --- /dev/null +++ b/.env.example @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: 2025 SecPal +# SPDX-License-Identifier: CC0-1.0 + +APP_NAME=SecPal +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost:8000 + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=secpal +DB_USERNAME=secpal +DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_SCHEME=null +MAIL_HOST=localhost +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="no-reply@secpal.dev" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" + +# Encryption Keys +KEK_PATH=/home/appuser/.secrets/kek +KEY_CACHE_TTL=5 +KEY_ROTATION_BATCH_SIZE=100 +KEY_ROTATION_CHECKPOINT=500 + +# Translation.io +# Get your API key from https://translation.io +# For Open Source projects: free unlimited account when project is public +TRANSLATIONIO_KEY=your_translation_io_key_here diff --git a/tests/Feature/TranslationConfigTest.php b/tests/Feature/TranslationConfigTest.php new file mode 100644 index 0000000..74faca0 --- /dev/null +++ b/tests/Feature/TranslationConfigTest.php @@ -0,0 +1,82 @@ +toBeArray('Translation config should be an array') + ->not->toBeEmpty('Translation config should not be empty'); +}); + +test('translation config has required keys', function () { + expect(config()->has('translation.key'))->toBeTrue('Config should have "key" entry'); + expect(config()->has('translation.source_locale'))->toBeTrue('Config should have "source_locale" entry'); + expect(config()->has('translation.target_locales'))->toBeTrue('Config should have "target_locales" entry'); + expect(config()->has('translation.gettext_parse_paths'))->toBeTrue('Config should have "gettext_parse_paths" entry'); +}); + +test('source locale is english', function () { + $sourceLocale = config('translation.source_locale'); + + expect($sourceLocale)->toBe('en', 'Source locale should be English (en)'); +}); + +test('target locales include german', function () { + $targetLocales = config('translation.target_locales'); + + expect($targetLocales) + ->toBeArray() + ->toContain('de'); +}); + +test('gettext parse paths are valid directories', function () { + $parsePaths = config('translation.gettext_parse_paths'); + + expect($parsePaths) + ->toBeArray('Parse paths should be an array') + ->not->toBeEmpty('Parse paths should not be empty'); + + foreach ($parsePaths as $path) { + $fullPath = base_path($path); + expect($fullPath)->toBeDirectory("Parse path '{$path}' should exist as directory at {$fullPath}"); + } +}); + +test('gettext parse paths include app and resources', function () { + $parsePaths = config('translation.gettext_parse_paths'); + + expect($parsePaths) + ->toContain('app') + ->toContain('resources'); +}); + +test('api key is loaded from environment', function () { + // In test environment, TRANSLATIONIO_KEY might not be set + // This test verifies that the config attempts to load it from env + $apiKey = config('translation.key'); + + // Should be null in test environment unless explicitly set + // The important thing is that it reads from env() without error + expect($apiKey === null || is_string($apiKey)) + ->toBeTrue('API key should be null or string when loaded from environment'); +}); + +test('translation config structure matches expected format', function () { + $config = config('translation'); + + // Verify structure + expect($config) + ->toHaveKey('key') + ->toHaveKey('source_locale') + ->toHaveKey('target_locales') + ->toHaveKey('gettext_parse_paths'); + + // Verify types + expect($config['key'] === null || is_string($config['key']))->toBeTrue(); + expect($config['source_locale'])->toBeString(); + expect($config['target_locales'])->toBeArray(); + expect($config['gettext_parse_paths'])->toBeArray(); +}); From cff93f30820e82b5855283f5cc34f3963446f60f Mon Sep 17 00:00:00 2001 From: Holger Schmermbeck Date: Sun, 2 Nov 2025 21:45:12 +0100 Subject: [PATCH 3/3] fix: correct copyright to 'SecPal Contributors' - Update config/translation.php copyright header - Update tests/Feature/TranslationConfigTest.php copyright header Follows project convention of using 'SecPal Contributors' for SPDX-FileCopyrightText as seen in other project files. --- config/translation.php | 2 +- tests/Feature/TranslationConfigTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/translation.php b/config/translation.php index 7625688..35c3b8a 100644 --- a/config/translation.php +++ b/config/translation.php @@ -1,6 +1,6 @@