Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- feat[!!!]: skip KeyNamingConventionValidator by default (opt-in to reduce false positives)
- feat: provide a standalone PHAR distribution for CI and non-Composer projects
- perf: minor render and file-collection optimizations
- perf: dedupe render keys via hash set instead of in_array
- perf: memoize placeholder extraction per value
- perf: memoize segment convention detection
- perf: return bool from validator validate() instead of building issue arrays
- perf: reuse parser-read file content in encoding validation
- perf: scan each directory once and bucket files by parser
- perf: evict parser cache per file set to bound memory
- perf: cache validator severity and drop per-comparison reflection
- perf: memoize extractKeys() results in parsers
- perf: cache prepared XLIFF schema source per version
- perf: resolve XLIFF content via a memoized id map
- fix: reject DOCTYPE and disable network access in XLIFF parser to harden against XXE and entity-expansion attacks
- fix: skip translation files exceeding a size limit to prevent memory exhaustion
- fix: strip control characters and escape markup in CLI output to prevent terminal injection
- fix: harden system path safety check with boundary matching
- fix: do not follow symlinks during recursive file scan
- fix: reject invalid validator classes in --only/--skip options instead of silently ignoring them
- fix: exclude PHP config files from auto-detection to avoid accidental code execution
- fix: parse PHP translation files via AST instead of executing them to prevent remote code execution

## [1.5.0] - 2026-06-29

Expand Down
87 changes: 87 additions & 0 deletions PERFORMANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Performance Baseline

Tracks wall-clock and memory numbers for `ValidationOrchestrationService::executeValidation()`
so the impact of the open `perf/*` PRs can be measured before/after they land.

## Methodology

- **Fixture set**: synthetic, generated (not part of the test suite) — 25 domains × 2 languages
(`en`/`de`) YAML files, 500 keys each (10 sections × 10 groups × 5 keys), 3 levels deep.
50 files, ~24,925 keys total. A small percentage of entries carry placeholders, HTML tags,
missing translations, or empty values so every validator has real work to do (not a zero-issue
run).
- **What's measured**: `ValidationOrchestrationService::executeValidation()` called directly
(bypassing the Composer command layer) with all validators from
`ValidatorRegistry::getAvailableValidators()` and default `TranslationValidatorConfig`,
against the fixture directory, non-recursive, auto-detected file grouping.
- **Runs**: 5 back-to-back invocations in the same PHP process; `gc_collect_cycles()` before each
run. Reported: min / median / mean / max wall time (ms) and peak memory (`memory_get_peak_usage(true)`).
Comment thread
konradmichalik marked this conversation as resolved.
Outdated
- **Environment**: PHP 8.5.0 (CLI, NTS), Apple M4 Pro, 14 cores, 24 GB RAM, macOS.
- Scripts used are not part of the repository (kept in a scratch location); the fixture generator
and benchmark runner can be recreated on request if this needs to become a repeatable CI check.

## Baseline: `main` @ `c570fe0` (2026-07-28)

| Metric | Value |
|---|---|
| Files checked | 50 |
| Keys checked | 24,925 |
| Min | 742.34 ms |
| Median | 758.25 ms |
| Mean | 784.89 ms |
| Max | 897.95 ms |
| Peak memory | 32.00 MB |

Raw durations (ms): `742.34, 748.93, 758.25, 776.97, 897.95`

## Performance PRs reflected below

All merged into `main` via squash, one after another, 2026-07-28:

- #137 fix: parse PHP translation files via AST instead of executing them
- #138 fix: exclude PHP config files from auto-detection
- #139 fix: reject invalid validator classes in --only/--skip options
- #140 fix: do not follow symlinks during recursive file scan
- #141 fix: harden system path safety check with boundary matching
- #142 fix: strip control characters and escape markup in CLI output
- #143 fix: skip translation files exceeding a size limit
- #144 fix: reject DOCTYPE and disable network in XLIFF parser
- #145 perf: resolve XLIFF content via a memoized id map
- #146 perf: cache prepared XLIFF schema source per version
- #147 perf: memoize extractKeys() results in parsers
- #148 perf: cache validator severity and drop per-comparison reflection
- #149 perf: evict parser cache per file set to bound memory
- #150 perf: scan each directory once and bucket files by parser
- #151 perf: reuse parser-read file content in encoding validation
- #152 perf: return bool from validator validate() instead of building issue arrays
- #153 perf: memoize segment convention detection
- #154 perf: memoize placeholder extraction per value
- #155 perf: dedupe render keys via hash set instead of in_array
- #156 perf: minor render and file-collection optimizations
- #157 feat: add standalone PHAR distribution
- #158 feat: disable KeyNamingConventionValidator by default

## After: `main` @ `f8910bc` (2026-07-28)

Same fixture set, same methodology (5 runs), run immediately after the merge sequence above.

| Metric | Before (`c570fe0`) | After (`f8910bc`) | Δ |
|---|---|---|---|
| Files checked | 50 | 50 | — |
| Keys checked | 24,925 | 24,925 | — |
| Min | 742.34 ms | 565.73 ms | **-23.8%** |
| Median | 758.25 ms | 566.42 ms | **-25.3%** |
| Mean | 784.89 ms | 573.74 ms | **-26.9%** |
| Max | 897.95 ms | 601.49 ms | **-33.0%** |
| Peak memory | 32.00 MB | 28.00 MB | **-12.5%** |

Raw durations (ms): `565.73, 565.96, 566.42, 569.10, 601.49`

The combined effect of the 22 merged PRs (8 security/correctness fixes, 12 performance
optimizations, 2 features) is a validation run that is roughly a **quarter faster** and uses
**~12.5% less peak memory** on the same workload, with no change in what was validated (same file
and key counts). Note that #158 (disabling `KeyNamingConventionValidator` by default) is a
behavioral default change, not a code path optimization, and is not exercised by this benchmark
since it calls `ValidatorRegistry::getAvailableValidators()` directly rather than the
config-resolved default set — the perf gain above is attributable to the `perf/*` and `fix/*`
changes to the parsing/collection/rendering hot paths.
2 changes: 1 addition & 1 deletion docs/configuration/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ only:
## skip

- **Type**: `array<string>`
- **Default**: `['DuplicateValuesValidator']`
- **Default**: `['DuplicateValuesValidator', 'KeyNamingConventionValidator']`
- **Description**: Array of validator class names to skip
Comment thread
konradmichalik marked this conversation as resolved.
Outdated

```yaml
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,6 @@ composer validate-translations ./translations
| [EncodingValidator](/reference/validators#encodingvalidator) | WARNING | Validates UTF-8 encoding and Unicode issues |
| [KeyCountValidator](/reference/validators#keycountvalidator) | WARNING | Warns when files exceed key threshold |
| [KeyDepthValidator](/reference/validators#keydepthvalidator) | WARNING | Warns about excessive nesting depth |
| [KeyNamingConventionValidator](/reference/validators#keynamingconventionvalidator) | WARNING | Enforces key naming patterns |
| [KeyNamingConventionValidator](/reference/validators#keynamingconventionvalidator) | WARNING | Enforces key naming patterns (opt-in) |

See the [Validators Reference](/reference/validators) for detailed documentation.
58 changes: 40 additions & 18 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ The command is also available under the alias `vt`.
Either a path to translation files must be provided as a command argument or within the configuration file. If no path is provided, the validator will abort.
:::

::: warning
If `--only` or `--skip` is given a value that resolves to no valid validator classes (e.g. a typo in the FQCN), the command fails with an error rather than silently falling back to running all validators.
:::

## Examples

### Basic Validation
Expand Down Expand Up @@ -123,10 +127,10 @@ composer validate-translations ./translations --dry-run

| Code | Description |
|------|-------------|
| `0` | Validation passed (no errors) |
| `1` | Validation failed (errors found) |
| `0` | Validation passed (no errors, or only warnings without `--strict`) |
| `1` | Validation failed (errors found, or warnings found with `--strict`) |

In `--dry-run` mode, the exit code is always `0` regardless of validation results.
In `--dry-run` mode, errors no longer produce a non-zero exit code. This does **not** apply to `--strict`: warnings still produce exit code `1` under `--strict` even when combined with `--dry-run`.

## Output Formats

Expand All @@ -145,34 +149,52 @@ translations/messages.en.yaml

### JSON Format

Machine-readable JSON output:
Machine-readable JSON output. Issues are keyed by file path, then by validator short name:

```json
{
"success": false,
"files": [
{
"path": "translations/messages.en.yaml",
"issues": [
{
"validator": "MismatchValidator",
"type": "error",
"message": "the translation key `delete` is missing"
}
]
"status": 1,
"message": "Language validation failed with errors.",
"issues": {
"translations/messages.en.yaml": {
"MismatchValidator": {
"type": "Error",
"issues": [
{
"message": "the translation key `delete` is missing but present in other files",
"details": {
"key": "delete"
}
}
]
}
}
]
},
"statistics": {
"execution_time": 0.012,
"execution_time_formatted": "12ms",
"files_checked": 1,
"keys_checked": 4,
"validators_run": 10,
"parsers_cached": 1
}
}
```

`status` is the same process exit code described under [Exit Codes](#exit-codes) above (`0` success, `1` failure). `details` varies per validator and mirrors the data available to `--verbose` CLI output.

### GitHub Format

Outputs GitHub Actions workflow commands for annotations:
Outputs GitHub Actions workflow commands for annotations, one per issue, followed by a summary annotation and a statistics notice:

```
::error file=translations/messages.en.yaml::the translation key `delete` is missing
::error file=translations/messages.en.yaml::the translation key `delete` is missing but present in other files
::error::Language validation failed with errors.
::notice::Validation completed in 12ms - Files: 1, Keys: 4, Validators: 10
```
Comment thread
konradmichalik marked this conversation as resolved.
Outdated

Annotations additionally include `line=`/`col=` when a validator reports a source position (e.g. `XliffSchemaValidator`), and `title=` when available.

## See Also

- [Configuration File](/configuration/config-file)
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ The Composer Translation Validator supports multiple translation file formats co

- XML-based industry standard format
- Supports translation metadata and states
- Schema validation available (XLIFF 1.2)
- Schema validation available (XLIFF 1.2 and 2.0)
- Target-language consistency check between filename locale and declared `target-language`/`trgLang`
- Nested keys via ID dots: `user.greeting`

## YAML (YAML Ain't Markup Language)
Expand Down
17 changes: 13 additions & 4 deletions docs/reference/programmatic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,28 @@ $result = $service->executeValidation(

## Working with Results

The `executeValidation()` method returns a `ValidationResult` object:
The `executeValidation()` method returns a `ValidationResult` object, or `null` if no matching
files were found:

```php
if ($result === null) {
// No files found to validate
return;
}

if ($result->hasErrors()) {
foreach ($result->getIssues() as $issue) {
echo $issue->getFilePath() . ': ' . $issue->getMessage() . PHP_EOL;
if ($result->hasIssues()) {
foreach ($result->getValidatorsWithIssues() as $validator) {
foreach ($validator->getIssues() as $issue) {
echo $issue->getFile() . ': ' . $validator->formatIssueMessage($issue) . PHP_EOL;
}
}
}

// The exit code Composer/the CLI would use, respecting --dry-run and --strict
$exitCode = $result->getOverallResult()->resolveErrorToCommandExitCode(
dryRun: $config->getDryRun(),
strict: $config->getStrict(),
);
```

## Use Cases
Expand Down
22 changes: 11 additions & 11 deletions docs/reference/validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ navigation:
large-file.en.yaml

KeyCountValidator
- Warning File contains 339 translation keys, exceeds threshold of 300
- Warning File contains 339 translation keys, which exceeds the threshold of 300 keys

[WARNING] Language validation completed with warnings.
```
Expand All @@ -343,10 +343,10 @@ validator-settings:

```bash
# Run the validator using pre-created fixtures (from project root)
composer validate-translations Fixtures/examples/key-count --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyCountValidator" -v
composer -d tests validate-translations Fixtures/examples/key-count --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyCountValidator" -v

# Test with custom configuration (threshold: 100)
composer validate-translations Fixtures/examples/key-count --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyCountValidator" -v --config Fixtures/examples/key-count/translation-validator.yaml
composer -d tests validate-translations Fixtures/examples/key-count --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyCountValidator" -v --config Fixtures/examples/key-count/translation-validator.yaml
```

</details>
Expand Down Expand Up @@ -417,10 +417,10 @@ validator-settings:

```bash
# Run the validator using pre-created fixtures (from project root)
composer validate-translations -d tests Fixtures/examples/key-depth --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyDepthValidator" -v
composer -d tests validate-translations Fixtures/examples/key-depth --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyDepthValidator" -v

# Test with custom configuration (threshold: 5)
composer validate-translations -d tests Fixtures/examples/key-depth --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyDepthValidator" -v --config Fixtures/examples/key-depth/translation-validator.yaml
composer -d tests validate-translations Fixtures/examples/key-depth --only "MoveElevator\\ComposerTranslationValidator\\Validator\\KeyDepthValidator" -v --config Fixtures/examples/key-depth/translation-validator.yaml
```

</details>
Expand Down Expand Up @@ -493,9 +493,9 @@ User.Address: "Address" # Mixed styles
mixed.en.yaml

KeyNamingConventionValidator
- Warning key naming violation: `userEmail` does not follow snake_case (suggestion: `user_email`)
- Warning key naming violation: `user-phone` does not follow snake_case (suggestion: `user_phone`)
- Warning key naming violation: `User.Address` does not follow snake_case (suggestion: `user.address`)
- Warning key naming convention violation: `userEmail` does not follow the configured snake_case convention. Suggested: `user_email`
- Warning key naming convention violation: `user-phone` does not follow the configured snake_case convention. Suggested: `user_phone`
- Warning key naming convention violation: `User.Address` does not follow the configured snake_case convention. Suggested: `user.address`

[WARNING] Language validation completed with warnings.
```
Expand Down Expand Up @@ -630,9 +630,9 @@ email: "Gesendet an {email_address}" # Missing double braces
notifications.de.yaml

PlaceholderConsistencyValidator
- Warning placeholder inconsistency in key `welcome` - missing: {username}, extra: {benutzername}
- Warning placeholder inconsistency in key `order` - missing: {amount}, extra: {sum}
- Warning placeholder inconsistency in key `email` - extra: {{ email_address }}
- Warning placeholder inconsistency in translation key `welcome` - missing: {username}, extra: {benutzername}
- Warning placeholder inconsistency in translation key `order` - missing: {amount}, extra: {sum}
- Warning placeholder inconsistency in translation key `email` - extra: {{ email_address }}

+-----------------+----------------------------------+--------------------------------+
| Translation Key | notifications.de.yaml | notifications.en.yaml |
Expand Down
Loading