Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,15 @@ The following translation validators are available:
| `EmptyValuesValidator` | Finds empty or whitespace-only translation values. | XLIFF, YAML, JSON, PHP | <span style="color:orange">WARNING</span> |
| `EncodingValidator` | Validates file encoding, checks for BOM, invisible characters and Unicode normalization issues. | XLIFF, YAML, JSON, PHP | <span style="color:orange">WARNING</span> |
| `HtmlTagValidator` | Validates HTML tag consistency across translations. Checks for matching tags, proper nesting, and consistent attributes between languages. | XLIFF, YAML, JSON, PHP | <span style="color:orange">WARNING</span> |
| `KeyNamingConventionValidator` | Validates translation key naming conventions. Supports configurable patterns like snake_case, camelCase, dot.notation, kebab-case, and custom regex patterns. **Requires configuration to run.** | XLIFF, YAML, JSON, PHP | <span style="color:#FFCA28">WARNING</span> |
| `MismatchValidator` | This validator checks for keys that are present in some files but not in others. It helps to identify mismatches in translation keys across different translation files. | XLIFF, YAML, JSON, PHP | <span style="color:orange">WARNING</span> |
| `PlaceholderConsistencyValidator` | Validates placeholder consistency across files. | XLIFF, YAML, JSON, PHP | <span style="color:orange">WARNING</span> |
| `XliffSchemaValidator` | Validates the XML schema of translation files against the XLIFF standard. See available [schemas](https://github.com/symfony/translation/tree/6.4/Resources/schemas). | XLIFF | <span style="color:red">ERROR</span> |

### Validator-Specific Configuration

Some validators support additional configuration options. For detailed configuration instructions and examples, see [Validator Configuration](docs/validator-configuration.md).

## 🧑‍💻 Contributing

Please have a look at [`CONTRIBUTING.md`](CONTRIBUTING.md).
Expand Down
166 changes: 166 additions & 0 deletions docs/validator-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Validator-Specific Configuration

Some validators support additional configuration options that can be specified in your configuration file. This allows you to customize validator behavior to match your project's specific requirements.

## Configuration Format

Validator-specific settings are configured under the `validator-settings` key in your configuration file:

### YAML Configuration
```yaml
# translation-validator.yaml
paths:
- translations/

validator-settings:
ValidatorName:
setting1: value1
setting2: value2
```

### JSON Configuration
```json
{
"paths": ["translations/"],
"validator-settings": {
"ValidatorName": {
"setting1": "value1",
"setting2": "value2"
}
}
}
```

## Available Validators

### KeyNamingConventionValidator

Configure naming conventions for translation keys. **This validator only runs when explicitly configured.**

#### Configuration Options

- `convention` - Use a predefined naming convention (see available conventions below)
- `custom_pattern` - Define your own regex pattern (overrides convention if both are provided)

#### YAML Configuration Example
```yaml
# translation-validator.yaml
paths:
- translations/

validator-settings:
KeyNamingConventionValidator:
convention: snake_case # Available: snake_case, camelCase, dot.notation, kebab-case, PascalCase
# OR use a custom regex pattern:
# custom_pattern: '/^[a-z][a-z0-9_]*$/'
```

#### JSON Configuration Example
```json
{
"paths": ["translations/"],
"validator-settings": {
"KeyNamingConventionValidator": {
"convention": "camelCase"
}
}
}
```

#### Available Conventions

- `snake_case` - user_name, form_submit
- `camelCase` - userName, formSubmit
- `dot.notation` - user.name, form.submit
- `kebab-case` - user-name, form-submit
- `PascalCase` - UserName, FormSubmit
- `custom_pattern` - Define your own regex pattern

#### Usage Examples

```bash
# Using config file
composer validate-translations translations/ --config translation-validator.yaml

# Using command-line (only to enable/disable, not configure)
composer validate-translations translations/ --only KeyNamingConventionValidator
```

#### Custom Pattern Examples

```yaml
validator-settings:
KeyNamingConventionValidator:
# Only lowercase letters and numbers
custom_pattern: '/^[a-z0-9]+$/'

# Specific prefix requirement
custom_pattern: '/^app\.[a-z][a-z0-9_]*$/'

# Maximum length constraint
custom_pattern: '/^[a-z][a-z0-9_]{0,29}$/' # Max 30 characters
```

## Creating Custom Validator Configurations

If you're developing a custom validator that supports configuration:

1. Implement the `setConfig()` method in your validator:
```php
public function setConfig(?TranslationValidatorConfig $config): void
{
$this->config = $config;
$this->loadSettingsFromConfig();
}

private function loadSettingsFromConfig(): void
{
if (null === $this->config) {
return;
}

$settings = $this->config->getValidatorSettings('YourValidatorClassName');

// Load your specific settings
if (isset($settings['your_setting'])) {
$this->yourSetting = $settings['your_setting'];
}
}
```

2. Add configuration validation and error handling:
```php
private function loadSettingsFromConfig(): void
{
// ... existing code ...

if (isset($settings['your_setting'])) {
if (!is_string($settings['your_setting'])) {
$this->logger?->warning('Invalid setting type for your_setting');
return;
}
$this->yourSetting = $settings['your_setting'];
}
}
```

3. Implement conditional execution if needed:
```php
public function shouldRun(): bool
{
return null !== $this->yourRequiredSetting;
}
```

## Configuration File Locations

The validator will automatically search for configuration files in the following order:

1. File specified with `--config` option
2. `translation-validator.php`
3. `translation-validator.json`
4. `translation-validator.yaml`
5. `translation-validator.yml`
6. Configuration referenced in `composer.json`

For more information about configuration files in general, see [Configuration Files](config-file.md).
33 changes: 33 additions & 0 deletions schema/translation-validator.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,28 @@
"type": "boolean",
"description": "Enable verbose output",
"default": false
},
"validator-settings": {
"type": "object",
"description": "Validator-specific configuration settings",
"patternProperties": {
"^[A-Za-z_][A-Za-z0-9_]*(?:\\\\\\\\[A-Za-z_][A-Za-z0-9_]*)*$": {
"type": "object",
"description": "Settings for a specific validator",
"additionalProperties": true
}
},
"additionalProperties": false,
"examples": [
{
"KeyNamingConventionValidator": {
"convention": "snake_case"
},
"HtmlTagValidator": {
"strict_attributes": true
}
}
]
}
},
"required": ["paths"],
Expand Down Expand Up @@ -143,6 +165,17 @@
"dry-run": false,
"format": "cli",
"verbose": true
},
{
"paths": ["translations"],
"validator-settings": {
"KeyNamingConventionValidator": {
"convention": "snake_case"
},
"HtmlTagValidator": {
"strict_attributes": false
}
}
}
]
}
5 changes: 3 additions & 2 deletions src/Command/ValidateTranslationCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ protected function configure(): void
->setDescription('Validates translation files with several validators.')
->addArgument(
'path',
InputArgument::IS_ARRAY | InputArgument::REQUIRED,
InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
'Paths to the folders containing translation files',
)
->addOption(
Expand Down Expand Up @@ -136,6 +136,7 @@ protected function configure(): void
• <info>EmptyValuesValidator</info> - Finds empty or whitespace-only translation values
• <info>EncodingValidator</info> - Validates file encoding and character issues
• <info>HtmlTagValidator</info> - Validates HTML tag consistency across translations
• <info>KeyNamingConventionValidator</info> - Validates translation key naming conventions
• <info>PlaceholderConsistencyValidator</info> - Validates placeholder consistency across files
• <info>XliffSchemaValidator</info> - Validates XLIFF schema compliance

Expand Down Expand Up @@ -203,7 +204,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$fileSets = ValidationRun::createFileSetsFromArray($allFiles);

$validationRun = new ValidationRun($this->logger);
$validationResult = $validationRun->executeFor($fileSets, $validators);
$validationResult = $validationRun->executeFor($fileSets, $validators, $config);

$format = FormatType::tryFrom($input->getOption('format') ?: $config->getFormat());

Expand Down
5 changes: 5 additions & 0 deletions src/Config/ConfigFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,10 @@ private function applyConfigurationSettings(TranslationValidatorConfig $config,
$config->$method($data[$key]);
}
}

// Handle validator-specific settings
if (isset($data['validator-settings']) && is_array($data['validator-settings'])) {
$config->setValidatorSettings($data['validator-settings']);
}
}
}
56 changes: 56 additions & 0 deletions src/Config/TranslationValidatorConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ class TranslationValidatorConfig

private bool $verbose = false;

/** @var array<string, array<string, mixed>> */
private array $validatorSettings = [];

/**
* @param string[] $paths
*/
Expand Down Expand Up @@ -263,6 +266,58 @@ public function getVerbose(): bool
return $this->verbose;
}

/**
* Set validator-specific settings.
*
* @param array<string, array<string, mixed>> $validatorSettings
*/
public function setValidatorSettings(array $validatorSettings): self
{
$this->validatorSettings = $validatorSettings;

return $this;
}

/**
* Set settings for a specific validator.
*
* @param array<string, mixed> $settings
*/
public function setValidatorSetting(string $validatorName, array $settings): self
{
$this->validatorSettings[$validatorName] = $settings;

return $this;
}

/**
* Get settings for a specific validator.
*
* @return array<string, mixed>
*/
public function getValidatorSettings(string $validatorName): array
{
return $this->validatorSettings[$validatorName] ?? [];
}

/**
* Get all validator settings.
*
* @return array<string, array<string, mixed>>
*/
public function getAllValidatorSettings(): array
{
return $this->validatorSettings;
}

/**
* Check if a validator has specific settings.
*/
public function hasValidatorSettings(string $validatorName): bool
{
return isset($this->validatorSettings[$validatorName]);
}

/**
* @return array<string, mixed>
*/
Expand All @@ -280,6 +335,7 @@ public function toArray(): array
'dry-run' => $this->dryRun,
'format' => $this->format,
'verbose' => $this->verbose,
'validator-settings' => $this->validatorSettings,
];
}
}
36 changes: 31 additions & 5 deletions src/FileDetector/Collector.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,18 @@ public function collectFiles(
if (null !== $detector) {
$allFiles[$parserClass][$path] = $detector->mapTranslationSet($files);
} else {
foreach (FileDetectorRegistry::getAvailableFileDetectors() as $fileDetector) {
$translationSet = (new $fileDetector())->mapTranslationSet($files);
if (!empty($translationSet)) {
$allFiles[$parserClass][$path] = $translationSet;
continue 2;
// Group files by directory to prevent cross-directory FileSets
$filesByDirectory = $this->groupFilesByDirectory($files);

foreach ($filesByDirectory as $directory => $directoryFiles) {
foreach (FileDetectorRegistry::getAvailableFileDetectors() as $fileDetector) {
$translationSet = (new $fileDetector())->mapTranslationSet($directoryFiles);
if (!empty($translationSet)) {
// Use directory-specific path key to separate FileSets
$pathKey = $path.'/'.$directory;
$allFiles[$parserClass][$pathKey] = $translationSet;
break; // Found a detector for this directory, move to next directory
}
}
}
}
Expand Down Expand Up @@ -182,4 +189,23 @@ private function isPathSafe(string $path): bool

return substr_count($path, '/') + substr_count($path, '\\') <= 20;
}

/**
* Groups files by their immediate parent directory to prevent cross-directory FileSets.
*
* @param array<string> $files
*
* @return array<string, array<string>>
*/
private function groupFilesByDirectory(array $files): array
{
$groups = [];

foreach ($files as $file) {
$directory = dirname($file);
$groups[$directory][] = $file;
}

return $groups;
}
}
Loading