Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
32 changes: 23 additions & 9 deletions src/Mcamara/LaravelLocalization/LaravelLocalization.php
Original file line number Diff line number Diff line change
Expand Up @@ -653,18 +653,32 @@ public function checkLocaleInSupportedLocales($locale)
*/
protected function substituteAttributesInRoute($attributes, $route, $locale = null)
{
foreach ($attributes as $key => $value) {
if ($value instanceOf Interfaces\LocalizedUrlRoutable) {
$value = $value->getLocalizedRouteKey($locale);
// Single-pass replacement mirroring Laravel's RouteUrlGenerator::replaceNamedParameters:
// strips any ":column" binding hint from the placeholder name (it's only used for model
// resolution, not URL generation) and delegates value extraction to getRouteKey() /
// getLocalizedRouteKey(), exactly as the framework does.
$route = preg_replace_callback('/\{(\w+)(?::\w+)?(\?)?\}/', function ($matches) use ($attributes, $locale) {
$key = $matches[1];

if (!array_key_exists($key, $attributes)) {
return $matches[0];
}
elseif ($value instanceOf UrlRoutable) {
$value = $value->getRouteKey();

$value = $attributes[$key];

if ($value instanceof Interfaces\LocalizedUrlRoutable) {
return $value->getLocalizedRouteKey($locale);
}
$route = str_replace(array('{'.$key.'}', '{'.$key.'?}'), $value, $route);
}

// delete empty optional arguments that are not in the $attributes array
$route = preg_replace('/\/{[^)]+\?}/', '', $route);
if ($value instanceof UrlRoutable) {
return $value->getRouteKey();
}

return $value;
}, $route);

// Remove remaining optional placeholders that were not provided in $attributes.
$route = preg_replace('/\/{[^}]+\?}/', '', $route);

return $route;
}
Expand Down
121 changes: 121 additions & 0 deletions tests/LaravelLocalizationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ protected function setRoutes($locale = false)
Route::get(app('laravellocalization')->transRoute('LaravelLocalization::routes.manage'), function () {
return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available';
})->name('manage');

Route::get(app('laravellocalization')->transRoute('LaravelLocalization::routes.view_with_slug'), function (string $post) {
return $post;
})->name('view_with_slug');

Route::get(app('laravellocalization')->transRoute('LaravelLocalization::routes.view_post_comment'), function (string $post, ?string $comment = null) {
return implode('/', array_filter([$post, $comment]));
})->name('view_post_comment');
});

Route::get('/skipped', function () {
Expand Down Expand Up @@ -585,6 +593,119 @@ public function testLocalizedParameterFromTranslateUrl(): void
);
}

#[DataProvider('customRouteKeySlugDataProvider')]
public function testGetURLFromRouteNameTranslatedWithCustomRouteKey(string $slug): void
{
$model = new ModelWithCustomRouteKey(['slug' => $slug]);

$this->assertEquals(
self::TEST_URL."en/posts/{$slug}",
app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.view_with_slug', ['post' => $model])
);

$this->assertEquals(
self::TEST_URL."es/publicaciones/{$slug}",
app('laravellocalization')->getURLFromRouteNameTranslated('es', 'LaravelLocalization::routes.view_with_slug', ['post' => $model])
);
}

public function testGetURLFromRouteNameTranslatedWithMultipleCustomRouteKeys(): void
{
$category = new ModelWithCustomRouteKey(['slug' => 'my-category']);
$post = new ModelWithCustomRouteKey(['slug' => 'my-post']);

$this->assertEquals(
self::TEST_URL.'en/posts/my-category/my-post',
app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.view_category_post', ['category' => $category, 'post' => $post])
);

$this->assertEquals(
self::TEST_URL.'es/publicaciones/my-category/my-post',
app('laravellocalization')->getURLFromRouteNameTranslated('es', 'LaravelLocalization::routes.view_category_post', ['category' => $category, 'post' => $post])
);
}

public function testGetURLFromRouteNameTranslatedWithOptionalCustomRouteKeyProvided(): void
{
$post = new ModelWithCustomRouteKey(['slug' => 'my-post']);
$comment = new ModelWithCustomRouteKey(['slug' => 'my-comment']);

$this->assertEquals(
self::TEST_URL.'en/posts/my-post/my-comment',
app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.view_post_comment', ['post' => $post, 'comment' => $comment])
);

$this->assertEquals(
self::TEST_URL.'es/publicaciones/my-post/my-comment',
app('laravellocalization')->getURLFromRouteNameTranslated('es', 'LaravelLocalization::routes.view_post_comment', ['post' => $post, 'comment' => $comment])
);
}

public function testGetURLFromRouteNameTranslatedWithOptionalCustomRouteKeyOmitted(): void
{
$post = new ModelWithCustomRouteKey(['slug' => 'my-post']);

$this->assertEquals(
self::TEST_URL.'en/posts/my-post',
app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.view_post_comment', ['post' => $post])
);

$this->assertEquals(
self::TEST_URL.'es/publicaciones/my-post',
app('laravellocalization')->getURLFromRouteNameTranslated('es', 'LaravelLocalization::routes.view_post_comment', ['post' => $post])
);
}

public function testGetURLFromRouteNameTranslatedWithColumnBindingAndStringValue(): void
{
$this->assertEquals(
self::TEST_URL.'en/posts/my-post',
app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.view_with_slug', ['post' => 'my-post'], true)
);

$this->assertEquals(
self::TEST_URL.'es/publicaciones/my-post',
app('laravellocalization')->getURLFromRouteNameTranslated('es', 'LaravelLocalization::routes.view_with_slug', ['post' => 'my-post'], true)
);
}

public function testRouteWithColumnBindingMatchesAndExtractsParameter(): void
{
$response = $this->get(self::TEST_URL.'posts/my-post');
$response->assertStatus(200);
$this->assertEquals('my-post', $response->getContent());
}

public function testRouteWithOptionalColumnBindingExtractsParameterWhenProvided(): void
{
$response = $this->get(self::TEST_URL.'posts/my-post/my-comment');
$response->assertStatus(200);
$this->assertEquals('my-post/my-comment', $response->getContent());
}

public function testRouteWithOptionalColumnBindingMatchesWhenOmitted(): void
{
$response = $this->get(self::TEST_URL.'posts/my-post');
$response->assertStatus(200);
$this->assertEquals('my-post', $response->getContent());
}
Comment on lines +686 to +691

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)LaravelLocalizationTest\.php$|route|routes|webpack|migrations|phpunit|composer' || true

echo
echo "Relevant test section:"
if [ -f tests/LaravelLocalizationTest.php ]; then
  wc -l tests/LaravelLocalizationTest.php
  sed -n '50,100p' tests/LaravelLocalizationTest.php
  sed -n '660,705p' tests/LaravelLocalizationTest.php
fi

echo
echo "Search route definitions in tests:"
rg -n "view_with_slug|view_post_comment|optional|comment|posts/my-post|Route::|fallback" tests -S || true

if command -v git >/dev/null 2>&1; then
  echo
  echo "Git diff summary (if available):"
  git diff --stat
  echo
  echo "Git diff for file (if available):"
  git diff -- tests/LaravelLocalizationTest.php | sed -n '1,240p'
fi

Repository: mcamara/laravel-localization

Length of output: 9483


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Candidate source files:"
git ls-files 'src/*.php' | sed -n '1,120p'

echo
echo "Search translateRoutes/transRoute:"
rg -n "function (translateRoutes|transRoute)|translateRoutes\(|transRoute\(" src -S

echo
echo "Relevant route/translation method bodies:"
for f in $(rg -l "translateRoutes|transRoute" src); do
  echo "--- $f"
  ast-grep outline "$f" --match 'translateRoutes' --view expanded || true
  ast-grep outline "$f" --match 'transRoute' --view expanded || true
done

echo
echo "Route collection refresh definition:"
rg -n "refresh_router_lookups|class LaravelLocalizationRouter|function getDefaultRouterLoader" tests src -S

Repository: mcamara/laravel-localization

Length of output: 2442


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "LaravelLocalization.php sections:"
wc -l src/Mcamara/LaravelLocalization/LaravelLocalization.php
sed -n '1,120p' src/Mcamara/LaravelLocalization/LaravelLocalization.php
sed -n '640,735p' src/Mcamara/LaravelLocalization/LaravelLocalization.php
sed -n '735,820p' src/Mcamara/LaravelLocalization/LaravelLocalization.php

echo
echo "Tests/LaravelLocalizationTest.php header/setup:"
sed -n '1,100p' tests/LaravelLocalizationTest.php

echo
echo "Language stub route generation/transRoute implementation searches:"
rg -n "translateRoutes|transRoute|cachedRoutes|translate|Route::group|Locale::make|routes\[" src/Mcamara/LaravelLocalization/LaravelLocalization.php src -S || true

echo
echo "Composer deps:"
sed -n '1,120p' composer.json

Repository: mcamara/laravel-localization

Length of output: 24487


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "setLocale definitions/usages:"
rg -n "function setLocale|public function setLocale|protected function setLocale|Locale:make|getNonLocalizedURL|getURLFromRouteNameTranslated|translations" src/Mcamara/LaravelLocalization/LaravelLocalization.php src/Mcamara/LaravelLocalization tests/tests src -S || true

echo
echo "Search route name assertions near optional binding tests:"
rg -n "assertStatus|assertRouteMatches|assertEquals\('my-post'|getContent\(\)|view_post_comment" tests/LaravelLocalizationTest.php -S || true

echo
echo "Framework doc evidence maybe via local docs if present:"
[ -d vendor ] && (find vendor/laravel/framework -maxdepth 4 -type f | rg 'Router|RouteCollection|Route' | head -20 || true) || true

Repository: mcamara/laravel-localization

Length of output: 8862


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Relevant Laravel routing source sections:"
for f in \
  vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php \
  vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php \
  vendor/laravel/framework/src/Illuminate/Routing/RouteUri.php \
  vendor/laravel/framework/src/Illuminate/Routing/RouteParameterBinder.php \
  vendor/laravel/framework/src/Illuminate/Routing/ImplicitRouteBinding.php \
  vendor/laravel/framework/src/Illuminate/Routing/RouteDependencyResolverTrait.php
do
  echo "--- $f"
  rg -n "function find|findRoute|getRoute|parameter|binding|slug|dependencies|dependency|RouteParameterBinder|resolveDependencies|dependencies" "$f" || true
done

echo
echo "Extract route matching/resolution snippets:"
python3 - <<'PY'
from pathlib import Path
targets = {
    "AbstractRouteCollection.php": [1, 180],
    "RouteCollection.php": [1, 180],
    "RouteUri.php": [1, 220],
    "RouteParameterBinder.php": [1, 220],
    "ImplicitRouteBinding.php": [1, 220],
    "RouteDependencyResolverTrait.php": [1, 180],
}
for path, (start, end) in targets.items():
    f = Path(f"vendor/laravel/framework/src/Illuminate/Routing/{path}")
    lines = f.read_text(errors="replace").splitlines()
    print(f"--- vendor/laravel/framework/src/Illuminate/Routing/{path}:{start}-{end}")
    for i in range(start-1, min(end, len(lines))):
        if any(tok in lines[i] for tok in ["function find", "function getRoute", "function resolveDependencies", "function getRouteKey", "function match", "parameter=", "dependency", "slug"]):
            print(f"{i+1}: {lines[i]}")
PY

Repository: mcamara/laravel-localization

Length of output: 6537


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "AbstractRouteCollection matching bodies:"
sed -n '60,140p' vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php
sed -n '180,230p' vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php

echo
echo "RouteCollection/CompiledRouteCollection dispatch body bodies:"
rg -n "function dispatch|function dispatchToRoute|protected function dispatch|public function dispatch" vendor/laravel/framework/src/Illuminate/Routing -S
case "$(rg -l "function dispatch\(" vendor/laravel/framework/src/Illuminate/Routing -S | head -1)" in
  vendor/laravel/framework/src/Illuminate/Routing/Router.php) sed -n '200,280p' vendor/laravel/framework/src/Illuminate/Routing/Router.php ;;
esac
sed -n '190,230p' vendor/laravel/framework/src/Illuminate/Routing/CompiledRouteCollection.php
sed -n '180,230p' vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php

echo
echo "ImplicitRouteBinding and dependencies bodies:"
sed -n '1,110p' vendor/laravel/apimachinery/src/Illuminate/Routing/ ... 2>/dev/null || sed -n '1,130p' vendor/laravel/framework/src/Illuminate/Routing/ImplicitRouteBinding.php
sed -n '1,180p' vendor/laravel/framework/src/Illuminate/Routing/RouteDependencyResolverTrait.php

echo
echo "Standalone Laravel RouteParameterBinder dependency/parameter probe if testbench autoload is available:"
php -r '
if (!file_exists("vendor/laravel/framework/src/Illuminate/Routing/Route.php")) exit("NO ROUTE CLASS");
$suffix = basename(getcwd());
if ($suffix !== "vendor") exit("In sandbox, avoiding actual request; source read-only inspection was requested.");
' || true

Repository: mcamara/laravel-localization

Length of output: 15144


Make the omitted-optional binding test unambiguous.

/posts/my-post can match both view_with_slug and view_post_comment. This test currently asserts the response body from either matching route, so it does not prove the optional comment segment is handled by view_post_comment; assert the matched route name or use a URI that only targets the optional route.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/LaravelLocalizationTest.php` around lines 686 - 691, Update
testRouteWithOptionalColumnBindingMatchesWhenOmitted to verify the
view_post_comment route is matched, either by asserting the matched route name
or by changing the request URI to one that cannot match view_with_slug, while
preserving validation of the omitted optional comment segment.


public static function customRouteKeySlugDataProvider(): array
{
return [
'simple slug' => ['my-post'],
'slug with hyphens' => ['my-long-post-title'],
'slug with underscores' => ['my_post_title'],
'slug with numbers' => ['post-123'],
'slug with hyphens and numbers' => ['2024-my-post-title'],
'slug with mixed hyphens and underscores' => ['my_post-title_here'],
'slug with dots' => ['v1.2.3-release'],
'numeric only slug' => ['123456'],
'slug with unicode characters' => ['über-den-wolken'],
'slug with accented characters' => ['cañon-del-rio'],
];
}

public function testGetNonLocalizedURL(): void
{
$this->assertEquals(
Expand Down
15 changes: 15 additions & 0 deletions tests/ModelWithCustomRouteKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Mcamara\LaravelLocalization\Tests;

use Illuminate\Database\Eloquent\Model;

class ModelWithCustomRouteKey extends Model
{
protected $fillable = ['slug'];

public function getRouteKeyName(): string
{
return 'slug';
}
}
3 changes: 3 additions & 0 deletions tests/lang/en/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
'about' => 'about',
'view' => 'view/{id}',
'view_project' => 'view/{id}/project/{project_id?}',
'view_with_slug' => 'posts/{post:slug}',
'view_category_post' => 'posts/{category:slug}/{post:slug}',
'view_post_comment' => 'posts/{post:slug}/{comment:slug?}',
'manage' => 'manage/{file_id?}',
'hello' => 'Hello world',
'test_text' => 'Test text',
Expand Down
3 changes: 3 additions & 0 deletions tests/lang/es/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
'about' => 'acerca',
'view' => 'ver/{id}',
'view_project' => 'ver/{id}/proyecto/{project_id?}',
'view_with_slug' => 'publicaciones/{post:slug}',
'view_category_post' => 'publicaciones/{category:slug}/{post:slug}',
'view_post_comment' => 'publicaciones/{post:slug}/{comment:slug?}',
'manage' => 'administrar/{file_id?}',
'hello' => 'Hola mundo',
'test_text' => 'Texto de prueba',
Expand Down