Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
85 changes: 85 additions & 0 deletions src/Enum/KeyNamingConvention.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Composer plugin "composer-translation-validator".
*
* Copyright (C) 2025 Konrad Michalik <km@move-elevator.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

namespace MoveElevator\ComposerTranslationValidator\Enum;

use InvalidArgumentException;

enum KeyNamingConvention: string
{
case SNAKE_CASE = 'snake_case';
case CAMEL_CASE = 'camelCase';
case KEBAB_CASE = 'kebab-case';
case PASCAL_CASE = 'PascalCase';
case DOT_NOTATION = 'dot.notation';

public function getPattern(): string
{
return match ($this) {
self::SNAKE_CASE => '/^[a-z]([a-z0-9]|_[a-z0-9])*$/',
self::CAMEL_CASE => '/^[a-z][a-zA-Z0-9]*$/',
self::KEBAB_CASE => '/^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]$/',
self::PASCAL_CASE => '/^[A-Z][a-zA-Z0-9]*$/',
self::DOT_NOTATION => '/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)*$/',
};
}

public function getDescription(): string
{
return match ($this) {
self::SNAKE_CASE => 'snake_case (lowercase with underscores)',
self::CAMEL_CASE => 'camelCase (first letter lowercase)',
self::KEBAB_CASE => 'kebab-case (lowercase with hyphens)',
self::PASCAL_CASE => 'PascalCase (first letter uppercase)',
self::DOT_NOTATION => 'dot.notation (lowercase with dots)',
};
}

/**
* Create enum instance from string value.
*
* @throws InvalidArgumentException if convention is not supported
*/
public static function fromString(string $convention): self
{
return self::tryFrom($convention) ?? throw new InvalidArgumentException(sprintf('Unknown convention "%s". Available conventions: %s', $convention, implode(', ', self::getAvailableConventions())));
}

/**
* Get all available convention names.
*
* @return array<string>
*/
public static function getAvailableConventions(): array
{
return array_map(fn (self $case): string => $case->value, self::cases());
}

/**
* Check if a key matches this convention.
*/
public function matches(string $key): bool
{
return 1 === preg_match($this->getPattern(), $key);
}
}
91 changes: 40 additions & 51 deletions src/Validator/KeyNamingConventionValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

use InvalidArgumentException;
use MoveElevator\ComposerTranslationValidator\Config\TranslationValidatorConfig;
use MoveElevator\ComposerTranslationValidator\Enum\KeyNamingConvention;
use MoveElevator\ComposerTranslationValidator\Parser\JsonParser;
use MoveElevator\ComposerTranslationValidator\Parser\ParserInterface;
use MoveElevator\ComposerTranslationValidator\Parser\PhpParser;
Expand All @@ -34,26 +35,7 @@

class KeyNamingConventionValidator extends AbstractValidator implements ValidatorInterface
{
private const CONVENTIONS = [
'snake_case' => [
'pattern' => '/^[a-z]([a-z0-9]|_[a-z0-9])*$/',
'description' => 'snake_case (lowercase with underscores)',
],
'camelCase' => [
'pattern' => '/^[a-z][a-zA-Z0-9]*$/',
'description' => 'camelCase (first letter lowercase)',
],
'kebab-case' => [
'pattern' => '/^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]$/',
'description' => 'kebab-case (lowercase with hyphens)',
],
'PascalCase' => [
'pattern' => '/^[A-Z][a-zA-Z0-9]*$/',
'description' => 'PascalCase (first letter uppercase)',
],
];

private ?string $convention = null;
private ?KeyNamingConvention $convention = null;
private ?string $customPattern = null;
private ?TranslationValidatorConfig $config = null;

Expand Down Expand Up @@ -136,11 +118,7 @@ private function loadConventionFromConfig(): void

public function setConvention(string $convention): void
{
if (!array_key_exists($convention, self::CONVENTIONS)) {
throw new InvalidArgumentException(sprintf('Unknown convention "%s". Available conventions: %s', $convention, implode(', ', array_keys(self::CONVENTIONS))));
}

$this->convention = $convention;
$this->convention = KeyNamingConvention::fromString($convention);
}

public function setCustomPattern(string $pattern): void
Expand Down Expand Up @@ -183,13 +161,11 @@ private function validateKeyFormat(string $key): bool

private function validateSegment(string $segment): bool
{
if (null === $this->convention || !isset(self::CONVENTIONS[$this->convention])) {
if (null === $this->convention) {
return true;
}

$pattern = self::CONVENTIONS[$this->convention]['pattern'];

return (bool) preg_match($pattern, $segment);
return $this->convention->matches($segment);
}

private function getActivePattern(): ?string
Expand All @@ -198,31 +174,25 @@ private function getActivePattern(): ?string
return $this->customPattern;
}

if (null !== $this->convention && isset(self::CONVENTIONS[$this->convention])) {
return self::CONVENTIONS[$this->convention]['pattern'];
}

return null;
return $this->convention?->getPattern();
}

private function suggestCorrection(string $key): string
{
if (null === $this->convention) {
return $key; // No suggestion for custom patterns
return $key;
}

// Handle dot-separated keys: convert each segment
if (str_contains($key, '.')) {
return $this->convertDotSeparatedKey($key);
}

// Single segment conversion
return match ($this->convention) {
'snake_case' => $this->toSnakeCase($key),
'camelCase' => $this->toCamelCase($key),
'kebab-case' => $this->toKebabCase($key),
'PascalCase' => $this->toPascalCase($key),
default => $key,
KeyNamingConvention::SNAKE_CASE => $this->toSnakeCase($key),
KeyNamingConvention::CAMEL_CASE => $this->toCamelCase($key),
KeyNamingConvention::KEBAB_CASE => $this->toKebabCase($key),
KeyNamingConvention::PASCAL_CASE => $this->toPascalCase($key),
KeyNamingConvention::DOT_NOTATION => $this->toDotNotation($key),
};
}

Expand Down Expand Up @@ -286,18 +256,29 @@ private function toPascalCase(string $key): string
return implode('', array_map('ucfirst', array_map('strtolower', $parts)));
}

private function toDotNotation(string $key): string
{
// Convert camelCase/PascalCase to dot.notation
$result = preg_replace('/([a-z])([A-Z])/', '$1.$2', $key);
// Convert snake_case and kebab-case to dot.notation
$result = str_replace(['_', '-'], '.', $result ?? $key);

return strtolower($result);
}

private function convertDotSeparatedKey(string $key): string
{
$segments = explode('.', $key);
$convertedSegments = [];

foreach ($segments as $segment) {
$convertedSegments[] = match ($this->convention) {
'snake_case' => $this->toSnakeCase($segment),
'camelCase' => $this->toCamelCase($segment),
'kebab-case' => $this->toKebabCase($segment),
'PascalCase' => $this->toPascalCase($segment),
default => $segment,
KeyNamingConvention::SNAKE_CASE => $this->toSnakeCase($segment),
KeyNamingConvention::CAMEL_CASE => $this->toCamelCase($segment),
KeyNamingConvention::KEBAB_CASE => $this->toKebabCase($segment),
KeyNamingConvention::PASCAL_CASE => $this->toPascalCase($segment),
KeyNamingConvention::DOT_NOTATION => $this->toDotNotation($segment),
null => $segment,
};
}

Expand Down Expand Up @@ -363,7 +344,15 @@ public function shouldShowDetailedOutput(): bool
*/
public static function getAvailableConventions(): array
{
return self::CONVENTIONS;
$conventions = [];
foreach (KeyNamingConvention::cases() as $convention) {
$conventions[$convention->value] = [
'pattern' => $convention->getPattern(),
'description' => $convention->getDescription(),
];
}

return $conventions;
}

/**
Expand Down Expand Up @@ -485,9 +474,9 @@ private function detectSegmentConventions(string $segment): array
{
$matchingConventions = [];

foreach (self::CONVENTIONS as $conventionName => $conventionData) {
if (preg_match($conventionData['pattern'], $segment)) {
$matchingConventions[] = $conventionName;
foreach (KeyNamingConvention::cases() as $convention) {
if ($convention->matches($segment)) {
$matchingConventions[] = $convention->value;
}
}

Expand Down