diff --git a/SCRATCHPAD.md b/SCRATCHPAD.md new file mode 100644 index 00000000000..47a3e7bd050 --- /dev/null +++ b/SCRATCHPAD.md @@ -0,0 +1,6 @@ +## Temp notes: + +Todo: Document TranslationServiceProvider must be added to config.php + +Copy localization.php to framework and add tests to ensure its up to date + diff --git a/_pages/index.blade.php b/_pages/index.blade.php index 7d6a48ff7af..e69de29bb2d 100644 --- a/_pages/index.blade.php +++ b/_pages/index.blade.php @@ -1,86 +0,0 @@ - - - - - - - - Welcome to HydePHP! - - - - - - - -
-
- -
-

- You're running on HydePHP -

-
-
-

- Leap into the future of static HTML blogs and documentation with the tools you already know and love. - Made with Tailwind, Laravel, and Coffee. -

-
- -
-

- This is the default homepage stored as index.blade.php, however you can publish any of the built-in views using the following command: - - -

php hyde publish:homepage
-

-
- -
- Resources for getting started - -
-
-
- -
-
- - diff --git a/_site/404.html b/_site/404.html new file mode 100644 index 00000000000..cc9fb2738c9 --- /dev/null +++ b/_site/404.html @@ -0,0 +1,13 @@ + + + + + + + + Redirecting to en/404.html + + + Redirecting to en/404.html. + + diff --git a/_site/en/404.html b/_site/en/404.html new file mode 100644 index 00000000000..6b3cd800f60 --- /dev/null +++ b/_site/en/404.html @@ -0,0 +1,48 @@ + + + + + 404 - Page not found + + + + + + + + + + + + + + + +
+
+
+
+ 404 +
+ +
+ +

+ Sorry, the page you are looking for could not be found. +

+ + + + +
+ +
+ +
+
+
+
+ + diff --git a/_site/en/index.html b/_site/en/index.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/_site/index.html b/_site/index.html new file mode 100644 index 00000000000..af38169243e --- /dev/null +++ b/_site/index.html @@ -0,0 +1,13 @@ + + + + + + + + Redirecting to en/index.html + + + Redirecting to en/index.html. + + diff --git a/_site/sitemap.xml b/_site/sitemap.xml new file mode 100644 index 00000000000..673c337a59f --- /dev/null +++ b/_site/sitemap.xml @@ -0,0 +1,2 @@ + +hydephp.com/en/404.html2026-07-11T22:32:45+00:00monthly0.25hydephp.com/sv/404.html2026-07-11T22:32:45+00:00monthly0.25hydephp.com/404.html2026-07-12T02:50:28+00:00monthly0.25hydephp.com/en/index.html2026-07-12T02:50:07+00:00daily1hydephp.com/sv/index.html2026-07-12T02:50:07+00:00daily1hydephp.com/index.html2026-07-12T02:50:28+00:00weekly0.5 diff --git a/_site/sv/404.html b/_site/sv/404.html new file mode 100644 index 00000000000..6b3cd800f60 --- /dev/null +++ b/_site/sv/404.html @@ -0,0 +1,48 @@ + + + + + 404 - Page not found + + + + + + + + + + + + + + + +
+
+
+
+ 404 +
+ +
+ +

+ Sorry, the page you are looking for could not be found. +

+ + + + +
+ +
+ +
+
+
+
+ + diff --git a/_site/sv/index.html b/_site/sv/index.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/config.php b/app/config.php index 26156665112..8011c43f8a8 100644 --- a/app/config.php +++ b/app/config.php @@ -74,6 +74,7 @@ Hyde\Framework\HydeServiceProvider::class, Hyde\Foundation\Providers\ViewServiceProvider::class, Hyde\Foundation\Providers\NavigationServiceProvider::class, + Hyde\Foundation\Providers\TranslationServiceProvider::class, Hyde\Console\ConsoleServiceProvider::class, ], diff --git a/composer.lock b/composer.lock index e99534abeaa..5f761cd6d3e 100644 --- a/composer.lock +++ b/composer.lock @@ -997,6 +997,7 @@ "require": { "hydephp/torchlight-commonmark": "^1.0", "illuminate/support": "^11.0", + "illuminate/translation": "^11.0", "illuminate/view": "^11.0", "league/commonmark": "^2.2", "php": "^8.2", @@ -1038,7 +1039,7 @@ }, { "name": "hyde/ui-kit", - "version": "dev-remove-publications", + "version": "dev-master", "dist": { "type": "path", "url": "./packages/ui-kit", @@ -2099,6 +2100,57 @@ }, "time": "2025-05-19T12:53:09+00:00" }, + { + "name": "illuminate/translation", + "version": "v11.51.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/translation.git", + "reference": "de64584470b1d4bfc30f2c291585b873ce844ecf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/translation/zipball/de64584470b1d4bfc30f2c291585b873ce844ecf", + "reference": "de64584470b1d4bfc30f2c291585b873ce844ecf", + "shasum": "" + }, + "require": { + "illuminate/collections": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/filesystem": "^11.0", + "illuminate/macroable": "^11.0", + "illuminate/support": "^11.0", + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Translation\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Translation package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-02-14T15:47:05+00:00" + }, { "name": "illuminate/view", "version": "v11.45.2", @@ -6883,7 +6935,7 @@ }, { "name": "hyde/monorepo-dev-tools", - "version": "dev-remove-publications", + "version": "dev-master", "dist": { "type": "path", "url": "./monorepo/DevTools", @@ -6913,7 +6965,7 @@ "dist": { "type": "path", "url": "./packages/realtime-compiler", - "reference": "dd4172f14d8d59e3de65a675132510c3095c947c" + "reference": "781f00bf731ff78845452a243cf305cdc11c2226" }, "require": { "desilva/microserve": "^2.0", @@ -6965,7 +7017,7 @@ }, { "name": "hyde/testing", - "version": "dev-remove-publications", + "version": "dev-master", "dist": { "type": "path", "url": "./packages/testing", diff --git a/config/localization.php b/config/localization.php new file mode 100644 index 00000000000..22b797b568d --- /dev/null +++ b/config/localization.php @@ -0,0 +1,8 @@ + [ + 'en', + 'sv', + ], +]; diff --git a/lang/en/main.php b/lang/en/main.php new file mode 100644 index 00000000000..921d5010ffd --- /dev/null +++ b/lang/en/main.php @@ -0,0 +1,5 @@ + 'Hello World!', +]; diff --git a/lang/sv/main.php b/lang/sv/main.php new file mode 100644 index 00000000000..4367b310460 --- /dev/null +++ b/lang/sv/main.php @@ -0,0 +1,11 @@ + 'Hello World!', +]; + +// lang/sv/main.php +return [ + 'welcome' => 'Hej Världen!', +]; diff --git a/packages/framework/composer.json b/packages/framework/composer.json index 1ef131b128f..5e6d547c039 100644 --- a/packages/framework/composer.json +++ b/packages/framework/composer.json @@ -25,6 +25,7 @@ "php": "^8.2", "illuminate/support": "^11.0", "illuminate/view": "^11.0", + "illuminate/translation": "^11.0", "symfony/yaml": "^7.0", "league/commonmark": "^2.2", "spatie/yaml-front-matter": "^2.0.9", diff --git a/packages/framework/config/localization.php b/packages/framework/config/localization.php new file mode 100644 index 00000000000..8a7e59656a0 --- /dev/null +++ b/packages/framework/config/localization.php @@ -0,0 +1,26 @@ + [ + // + ], +]; diff --git a/packages/framework/src/Facades/Localization.php b/packages/framework/src/Facades/Localization.php new file mode 100644 index 00000000000..9300d7b7193 --- /dev/null +++ b/packages/framework/src/Facades/Localization.php @@ -0,0 +1,73 @@ + 0; + } + + /** + * Get the languages the site is compiled for, in the order they are configured. + * + * @return array + */ + public static function languages(): array + { + return array_values(Config::getArray('localization.languages', [])); + } + + /** + * Get the default language of the site, which is the first configured language. + */ + public static function defaultLanguage(): string + { + return static::languages()[0] ?? Config::getString('app.locale', 'en'); + } + + /** + * Run the callback using the given language as the app locale, so that translation + * strings are resolved for it, then restore the previously active locale. + * + * Passing a null language runs the callback as is, using the default locale. + * + * @template T + * + * @param \Closure(): T $callback + * @return T + */ + public static function usingLanguage(?string $language, Closure $callback): mixed + { + if ($language === null) { + return $callback(); + } + + $locale = App::getLocale(); + + App::setLocale($language); + + try { + return $callback(); + } finally { + App::setLocale($locale); + } + } +} diff --git a/packages/framework/src/Foundation/Kernel/PageCollection.php b/packages/framework/src/Foundation/Kernel/PageCollection.php index 4cc4d85eac2..50442ed6d81 100644 --- a/packages/framework/src/Foundation/Kernel/PageCollection.php +++ b/packages/framework/src/Foundation/Kernel/PageCollection.php @@ -4,10 +4,16 @@ namespace Hyde\Foundation\Kernel; +use Hyde\Hyde; +use Hyde\Facades\Localization; use Hyde\Foundation\Concerns\BaseFoundationCollection; use Hyde\Framework\Exceptions\FileNotFoundException; use Hyde\Pages\Concerns\HydePage; use Hyde\Support\Filesystem\SourceFile; +use Hyde\Support\Models\Redirect; + +use function str_repeat; +use function substr_count; /** * The PageCollection contains all the instantiated pages. @@ -30,16 +36,51 @@ final class PageCollection extends BaseFoundationCollection { public function addPage(HydePage $page): void { - $this->put($page->getSourcePath(), $page); + $this->put(static::makeKey($page), $page); } protected function runDiscovery(): void { $this->kernel->files()->each(function (SourceFile $file): void { - $this->addPage($this->parsePage($file->pageClass, $file->getPath())); + $page = $this->parsePage($file->pageClass, $file->getPath()); + + if (Localization::enabled()) { + foreach (Localization::languages() as $language) { + $this->addPage($page->withLanguage($language)); + } + + $this->addDefaultLanguageRedirect($page); + } else { + $this->addPage($page); + } }); } + /** + * Add a redirect from the unprefixed route key to the default language, so that + * for example `/foo` sends the visitor to `/en/foo` when English is the default. + */ + protected function addDefaultLanguageRedirect(HydePage $page): void + { + $routeKey = $page->getRouteKey(); + + $this->addPage(new Redirect($routeKey, static::makeRedirectDestination($routeKey), matter: [ + 'navigation' => ['hidden' => true], + ])); + } + + protected static function makeRedirectDestination(string $routeKey): string + { + // The destination is relative to the redirect page, which sits at the unprefixed + // route key, so we need to walk back up to the site webroot before descending + // into the language directory. For example, `posts/hello` redirects to + // `../en/posts/hello.html`, which resolves to `/en/posts/hello.html`. + + $depth = substr_count($routeKey, '/'); + + return str_repeat('../', $depth).Hyde::formatLink(Localization::defaultLanguage()."/$routeKey.html"); + } + protected function runExtensionHandlers(): void { /** @var class-string<\Hyde\Foundation\Concerns\HydeExtension> $extension */ @@ -56,7 +97,24 @@ protected static function parsePage(string $pageClass, string $path): HydePage public function getPage(string $sourcePath): HydePage { - return $this->get($sourcePath) ?? throw new FileNotFoundException($sourcePath); + // When the site is localized, each source file has one page per language, + // so we resolve the page for the default language when given a source path. + + return $this->get($sourcePath) + ?? $this->get(static::makeLocalizedKey(Localization::defaultLanguage(), $sourcePath)) + ?? throw new FileNotFoundException($sourcePath); + } + + protected static function makeKey(HydePage $page): string + { + return $page->getLanguage() === null + ? $page->getSourcePath() + : static::makeLocalizedKey($page->getLanguage(), $page->getSourcePath()); + } + + protected static function makeLocalizedKey(string $language, string $sourcePath): string + { + return "$language::$sourcePath"; } /** @param class-string<\Hyde\Pages\Concerns\HydePage>|null $pageClass */ diff --git a/packages/framework/src/Foundation/Providers/TranslationServiceProvider.php b/packages/framework/src/Foundation/Providers/TranslationServiceProvider.php new file mode 100644 index 00000000000..e06ec1c1e05 --- /dev/null +++ b/packages/framework/src/Foundation/Providers/TranslationServiceProvider.php @@ -0,0 +1,32 @@ +app['config']->set('app.locale', $language); + $this->app['config']->set('app.fallback_locale', Config::getNullableString('app.fallback_locale') ?? $language); + + parent::register(); + } +} diff --git a/packages/framework/src/Framework/Actions/StaticPageBuilder.php b/packages/framework/src/Framework/Actions/StaticPageBuilder.php index ddd89c87613..2164728b31f 100644 --- a/packages/framework/src/Framework/Actions/StaticPageBuilder.php +++ b/packages/framework/src/Framework/Actions/StaticPageBuilder.php @@ -7,6 +7,7 @@ use Hyde\Hyde; use Hyde\Facades\Filesystem; use Hyde\Framework\Concerns\InteractsWithDirectories; +use Hyde\Facades\Localization; use Hyde\Pages\Concerns\HydePage; /** @@ -27,8 +28,17 @@ public static function handle(HydePage $page): string Hyde::shareViewData($page); - Filesystem::putContents($path, $page->compile()); + Filesystem::putContents($path, static::compilePage($page)); return $path; } + + /** + * Compile the page, using the page's language as the app locale when the site is localized, + * so that translation strings are resolved for the language the page is being built for. + */ + protected static function compilePage(HydePage $page): string + { + return Localization::usingLanguage($page->getLanguage(), fn (): string => $page->compile()); + } } diff --git a/packages/framework/src/Pages/Concerns/HydePage.php b/packages/framework/src/Pages/Concerns/HydePage.php index e60e44dc187..5944e0db034 100644 --- a/packages/framework/src/Pages/Concerns/HydePage.php +++ b/packages/framework/src/Pages/Concerns/HydePage.php @@ -67,6 +67,9 @@ abstract class HydePage implements PageSchema, SerializableContract public PageMetadataBag $metadata; public NavigationData $navigation; + /** The language the page is compiled for, if the site is localized. */ + protected ?string $language = null; + /** * Create a new page instance. Static alias for the constructor. */ @@ -284,7 +287,9 @@ public function getSourcePath(): string */ public function getOutputPath(): string { - return unslash(static::outputPath($this->identifier)); + return $this->language === null + ? unslash(static::outputPath($this->identifier)) + : "{$this->getRouteKey()}.html"; } // Section: Routing @@ -301,7 +306,30 @@ public function getOutputPath(): string */ public function getRouteKey(): string { - return $this->routeKey; + return $this->language === null ? $this->routeKey : unslash("{$this->language}/{$this->routeKey}"); + } + + /** + * Get the language the page is compiled for, or null if the site is not localized. + */ + public function getLanguage(): ?string + { + return $this->language; + } + + /** + * Get a copy of the page that is compiled for the given language. + * + * The page keeps its source file and Blade view, but is routed to a language + * subdirectory, and is compiled with the given language as the app locale. + */ + public function withLanguage(string $language): static + { + $page = clone $this; + + $page->language = $language; + + return $page; } /** diff --git a/packages/realtime-compiler/src/Http/DashboardController.php b/packages/realtime-compiler/src/Http/DashboardController.php index 5a46b46bcf1..e246f58a8c0 100644 --- a/packages/realtime-compiler/src/Http/DashboardController.php +++ b/packages/realtime-compiler/src/Http/DashboardController.php @@ -16,6 +16,7 @@ use Desilva\Microserve\Request; use Desilva\Microserve\Response; use Hyde\Facades\Filesystem; +use Hyde\Facades\Localization; use Hyde\Pages\Concerns\HydePage; use Hyde\Pages\DocumentationPage; use Hyde\Support\Models\RouteKey; @@ -280,7 +281,7 @@ public static function renderIndexPage(HydePage $page): string } else { Hyde::shareViewData($page); - $contents = $page->compile(); + $contents = Localization::usingLanguage($page->getLanguage(), fn (): string => $page->compile()); } if (self::isLoadedInIframe()) { diff --git a/packages/realtime-compiler/src/Routing/PageRouter.php b/packages/realtime-compiler/src/Routing/PageRouter.php index 4fbca1c5f8f..0f038776578 100644 --- a/packages/realtime-compiler/src/Routing/PageRouter.php +++ b/packages/realtime-compiler/src/Routing/PageRouter.php @@ -6,6 +6,7 @@ use Desilva\Microserve\Request; use Desilva\Microserve\Response; +use Hyde\Facades\Localization; use Hyde\Foundation\Facades\Routes; use Hyde\Support\Models\Route; use Hyde\Pages\Concerns\BaseMarkdownPage; @@ -94,7 +95,7 @@ protected function getHtml(HydePage $page): string } else { Hyde::shareViewData($page); - $contents = $page->compile(); + $contents = Localization::usingLanguage($page->getLanguage(), fn (): string => $page->compile()); } if ($page instanceof BaseMarkdownPage && LiveEditController::enabled()) {