diff --git a/resources/templates/_elements/toolbar.twig b/resources/templates/_elements/toolbar.twig index a0ac2731dca..ab1e8ce08fd 100644 --- a/resources/templates/_elements/toolbar.twig +++ b/resources/templates/_elements/toolbar.twig @@ -1,13 +1,5 @@ {% from "_includes/forms" import text -%} -{% do view.registerTranslations('app', [ - "Sort by {attribute}", - "Score", - "Structure", - "Display in a table", - "Display hierarchically", - "Display as thumbnails", -]) %} {% hook 'cp.elements.toolbar' %} diff --git a/resources/templates/_includes/permissions.twig b/resources/templates/_includes/permissions.twig index a047be92570..a8bbfb21193 100644 --- a/resources/templates/_includes/permissions.twig +++ b/resources/templates/_includes/permissions.twig @@ -1,9 +1,5 @@ {% requireEdition CraftTeam %} -{% do view.registerTranslations('app', [ - "Select All", - "Deselect All", -]) %} {% do view.registerAssetBundle("craft\\web\\assets\\userpermissions\\UserPermissionsAsset") %} diff --git a/resources/templates/_layouts/basecp.twig b/resources/templates/_layouts/basecp.twig index dc116cfc91d..ddd953f537e 100644 --- a/resources/templates/_layouts/basecp.twig +++ b/resources/templates/_layouts/basecp.twig @@ -5,10 +5,6 @@ {% set bodyClass = (bodyClass ?? [])|explodeClass|push('mobile') -%} {% endif %} -{% do view.registerTranslations('app', [ - "Show", - "Hide", -]) %} {% set localeData = craft.i18n.locale %} {% set orientation = localeData.getOrientation() %} diff --git a/resources/templates/_special/image_editor.twig b/resources/templates/_special/image_editor.twig index 26cce37ba89..a327cbbc304 100644 --- a/resources/templates/_special/image_editor.twig +++ b/resources/templates/_special/image_editor.twig @@ -1,10 +1,6 @@ {% import "_includes/forms" as forms %} {% set orientation = craft.i18n.locale.getOrientation() -%} -{% do view.registerTranslations('app', [ - 'Original', - 'Square', - 'Unconstrained', -]) %} + {{ tag('h1', { class: 'visually-hidden', text: 'Edit Image'|t('app'), diff --git a/resources/templates/_special/install/index.twig b/resources/templates/_special/install/index.twig index 49ae787342d..82c04104a54 100644 --- a/resources/templates/_special/install/index.twig +++ b/resources/templates/_special/install/index.twig @@ -1,10 +1,6 @@ {% extends "_layouts/basecp" %} {% set title = "Install Craft CMS"|t('app') %} -{% do view.registerTranslations('app', [ - "All done!", - "Go to Craft CMS", -]) %} {% block body %}
diff --git a/resources/templates/_special/licensing-issues.twig b/resources/templates/_special/licensing-issues.twig index 00abefa7e17..bf68d1fd99c 100644 --- a/resources/templates/_special/licensing-issues.twig +++ b/resources/templates/_special/licensing-issues.twig @@ -5,10 +5,6 @@ {% set bodyClass = 'licensing-issues' %} {% set title = 'License purchase required.'|t('app') %} -{% do view.registerTranslations('app', [ - 'Continue to the control panel', - 'Continue to the control panel in {num, number} {num, plural, =1{second} other{seconds}}', -]) %} {% block body %}
diff --git a/resources/templates/assets/_previews/image.twig b/resources/templates/assets/_previews/image.twig index 84c3c863255..64d5e6d6edd 100644 --- a/resources/templates/assets/_previews/image.twig +++ b/resources/templates/assets/_previews/image.twig @@ -28,12 +28,6 @@
{% do view.registerAssetBundle("craft\\web\\assets\\focalpoint\\FocalPointAsset") %} -{% do view.registerTranslations('app', [ - 'Enable focal point', - 'Disable focal point', - 'Saved', - 'Saving…', -]) %} {% if enableFocalPoint %} {% js %} diff --git a/resources/templates/graphql/schemas/_edit.twig b/resources/templates/graphql/schemas/_edit.twig index 68f41312144..bb592ae9df2 100644 --- a/resources/templates/graphql/schemas/_edit.twig +++ b/resources/templates/graphql/schemas/_edit.twig @@ -60,10 +60,6 @@ {% from _self import permissionList %} -{% do view.registerTranslations('app', [ - "Select All", - "Deselect All", -]) %} {% do view.registerAssetBundle("craft\\web\\assets\\userpermissions\\UserPermissionsAsset") %} diff --git a/resources/templates/graphql/tokens/_index.twig b/resources/templates/graphql/tokens/_index.twig index e1c5aed4183..2248e943e3e 100644 --- a/resources/templates/graphql/tokens/_index.twig +++ b/resources/templates/graphql/tokens/_index.twig @@ -6,12 +6,6 @@ {% set tokens = craft.app.gql.tokens %} {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} -{% do view.registerTranslations('app', [ - 'No GraphQL tokens exist yet.', - 'Never', - 'Last Used', - 'Expires', -]) %} {% block actionButton %} {{ "New token"|t('app') }} diff --git a/resources/templates/settings/assets/transforms/_index.twig b/resources/templates/settings/assets/transforms/_index.twig index 0ca08918441..4007cd8917c 100644 --- a/resources/templates/settings/assets/transforms/_index.twig +++ b/resources/templates/settings/assets/transforms/_index.twig @@ -4,15 +4,6 @@ {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} -{% do view.registerTranslations('app', [ - "Name", - "Handle", - "Mode", - "Dimensions", - "Interlace", - "Format", - "No image transforms exist yet.", -]) %} {% if readOnly %} {% set contentNotice = readOnlyNotice() %} diff --git a/resources/templates/settings/assets/volumes/_index.twig b/resources/templates/settings/assets/volumes/_index.twig index 65133211249..9d0bc5f514e 100644 --- a/resources/templates/settings/assets/volumes/_index.twig +++ b/resources/templates/settings/assets/volumes/_index.twig @@ -4,12 +4,6 @@ {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} -{% do view.registerTranslations('app', [ - "Name", - "Handle", - "Type", - "No volumes exist yet." -]) %} {% if readOnly %} {% set contentNotice = readOnlyNotice() %} diff --git a/resources/templates/settings/email/_index.twig b/resources/templates/settings/email/_index.twig index 2be033bc0be..f3bef0f6b54 100644 --- a/resources/templates/settings/email/_index.twig +++ b/resources/templates/settings/email/_index.twig @@ -21,9 +21,6 @@ {% import '_includes/forms.twig' as forms %} -{% do view.registerTranslations('app', [ - "Email sent successfully! Check your inbox.", -]) %} {% if settings is not defined %} diff --git a/resources/templates/settings/entry-types/index.twig b/resources/templates/settings/entry-types/index.twig index 7a228240244..057cc059a8d 100644 --- a/resources/templates/settings/entry-types/index.twig +++ b/resources/templates/settings/entry-types/index.twig @@ -5,16 +5,7 @@ {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} -{% do view.registerTranslations('app', [ - 'Are you sure you want to delete “{name}” and all entries of that type?', - 'Description', - 'Entry Type', - 'Handle', - 'No entry types exist yet.', - 'No results.', - 'No usages', - 'Used by', -]) %} + {% set title = 'Entry Types'|t('app') %} diff --git a/resources/templates/settings/fields/index.twig b/resources/templates/settings/fields/index.twig index 202a54036ce..a043626b054 100644 --- a/resources/templates/settings/fields/index.twig +++ b/resources/templates/settings/fields/index.twig @@ -3,16 +3,7 @@ {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} -{% do view.registerTranslations('app', [ - 'Handle', - 'Name', - 'No fields exist yet.', - 'No results.', - 'No usages', - 'This field’s values are used as search keywords.', - 'Type', - 'Used by', -]) %} + {% set crumbs = [ { label: "Settings"|t('app'), url: url('settings') } diff --git a/resources/templates/settings/filesystems/_index.twig b/resources/templates/settings/filesystems/_index.twig index 958ef6e3d8f..4fe580f6556 100644 --- a/resources/templates/settings/filesystems/_index.twig +++ b/resources/templates/settings/filesystems/_index.twig @@ -7,12 +7,6 @@ {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} -{% do view.registerTranslations('app', [ - "Name", - "Handle", - "Type", - "No filesystems exist yet." -]) %} {% block actionButton %} {% if not readOnly %} diff --git a/resources/templates/settings/routes.twig b/resources/templates/settings/routes.twig index 8f3d6a556b2..b45e12d752d 100644 --- a/resources/templates/settings/routes.twig +++ b/resources/templates/settings/routes.twig @@ -13,21 +13,7 @@ { label: "Settings"|t('app'), url: url('settings') } ] %} -{% do view.registerTranslations('app', [ - "Add a token", - "Are you sure you want to delete this route?", - "Couldn’t save new route order.", - "Couldn’t save route.", - "Create a new route", - "Edit Route", - "Global", - "If the URI looks like this", - "Load this template", - "New route order saved.", - "Route deleted.", - "Route Saved.", - "The URI can’t begin with the {setting} config setting.", -]) %} + {% set actionMenuItems = [ { diff --git a/resources/templates/settings/sections/_index.twig b/resources/templates/settings/sections/_index.twig index 23c8ad6e24b..66b53def2eb 100644 --- a/resources/templates/settings/sections/_index.twig +++ b/resources/templates/settings/sections/_index.twig @@ -5,17 +5,7 @@ {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} -{% do view.registerTranslations('app', [ - "Are you sure you want to delete “{name}” and all its entries?", - "Edit entry type", - "Edit entry types ({count})", - "Edit entry types", - "Entry Types", - "Handle", - "Name", - "No sections exist yet.", - "Type", -]) %} + {% set crumbs = [ { label: "Settings"|t('app'), url: url('settings') } diff --git a/resources/templates/settings/users/groups/_index.twig b/resources/templates/settings/users/groups/_index.twig index fb859b18d79..e09ee6bea58 100644 --- a/resources/templates/settings/users/groups/_index.twig +++ b/resources/templates/settings/users/groups/_index.twig @@ -7,11 +7,6 @@ {% do view.registerAssetBundle('craft\\web\\assets\\admintable\\AdminTableAsset') -%} -{% do view.registerTranslations('app', [ - "Name", - "Handle", - "No groups exist yet.", -]) %} {% set groups = craft.userGroups.getAllGroups() %} diff --git a/src/Support/Facades/HtmlStack.php b/src/Support/Facades/HtmlStack.php index 2b1bc61991d..dde4dcfa72f 100644 --- a/src/Support/Facades/HtmlStack.php +++ b/src/Support/Facades/HtmlStack.php @@ -16,7 +16,6 @@ * @method static void scriptWithVars(callable $fn, array $vars, \CraftCms\Cms\View\Enums\Position $position = 3, array $options = [], string|null $key = null) * @method static void html(string $html, \CraftCms\Cms\View\Enums\Position $position = 3, string|null $key = null) * @method static void jsImport(string $key, string $value) - * @method static void translations(array $messages, string $category = 'app') * @method static void icons(array $icons) * @method static void metaTag(array $attributes, string|null $key = null) * @method static void linkTag(array $attributes, string|null $key = null) diff --git a/src/Support/Facades/I18N.php b/src/Support/Facades/I18N.php index 3d0495b069a..fcb059bb865 100644 --- a/src/Support/Facades/I18N.php +++ b/src/Support/Facades/I18N.php @@ -24,6 +24,7 @@ * @method static \Illuminate\Support\Collection getEditableLocaleIds() * @method static string translate(\Stringable|string $message, array $parameters = [], string|null $category = null, string|null $locale = null) * @method static void addCategorySources(\Yiisoft\Translator\CategorySource ...$categories) + * @method static array getAllTranslationsForLocale(string $locale) * @method static string prep(string $message, array $params = [], ?string $category = null, ?string $locale = null) * * @see \CraftCms\Cms\Translation\I18N diff --git a/src/Translation/I18N.php b/src/Translation/I18N.php index 0750b3c5f67..67304172238 100644 --- a/src/Translation/I18N.php +++ b/src/Translation/I18N.php @@ -6,7 +6,6 @@ use Craft; use CraftCms\Cms\Cms; -use CraftCms\Cms\Config\GeneralConfig; use CraftCms\Cms\Site\Data\Site; use CraftCms\Cms\Support\Facades\Sites; use CraftCms\Cms\Support\Facades\Users; @@ -18,6 +17,7 @@ use InvalidArgumentException; use ResourceBundle; use Stringable; +use Yiisoft\I18n\Locale as YiisoftLocale; use Yiisoft\Translator\CategorySource; use Yiisoft\Translator\Translator; @@ -52,8 +52,12 @@ final class I18N */ private ?Collection $appLocales = null; + /** + * @var array> + */ + private array $registeredCategories = []; + public function __construct( - private readonly GeneralConfig $generalConfig, private readonly Translator $translator, ) {} @@ -91,8 +95,8 @@ public function getFormattingLocale(): Locale } } - if ($this->generalConfig->defaultCpLocale) { - return $this->getLocaleById($this->generalConfig->defaultCpLocale); + if (Cms::config()->defaultCpLocale) { + return $this->getLocaleById(Cms::config()->defaultCpLocale); } return $this->getLocale(); @@ -125,7 +129,7 @@ public function getAllLocaleIds(): Collection } $allLocaleIds = ResourceBundle::getLocales(''); - $this->localeAliases = $this->generalConfig->localeAliases; + $this->localeAliases = Cms::config()->localeAliases; // Hyphens, not underscores foreach ($allLocaleIds as $i => $locale) { @@ -315,7 +319,7 @@ public function translate(string|Stringable $message, array $parameters = [], ?s $translation = $this->translator->translate($message, $parameters, $category, $locale); - if ($this->generalConfig->translationDebugOutput) { + if (Cms::config()->translationDebugOutput) { $char = match ($category) { 'site' => '$', 'app' => '@', @@ -345,9 +349,68 @@ public function translate(string|Stringable $message, array $parameters = [], ?s public function addCategorySources(CategorySource ...$categories): void { + foreach ($categories as $category) { + $this->registeredCategories[$category->getName()][] = $category; + } + $this->translator->addCategorySources(...$categories); } + /** + * Returns all translations for a given locale, organized by category. + * + * Only includes messages where the translation differs from the source key. + * Handles locale fallback (e.g., `fr-CA` → `fr`). + * + * @return array> Nested array of `[category => [sourceMessage => translation]]` + */ + public function getAllTranslationsForLocale(string $locale): array + { + $result = []; + + // Normalize underscores to hyphens (BCP 47 format) + $locale = str_replace('_', '-', $locale); + + // Build the locale fallback chain (e.g., fr-CA → fr) + $localesToCheck = [$locale]; + $yiisoftLocale = new YiisoftLocale($locale); + $fallback = $yiisoftLocale->fallbackLocale(); + + if ($fallback->asString() !== $locale) { + // Prepend the fallback so the exact locale wins when merged on top + array_unshift($localesToCheck, $fallback->asString()); + } + + foreach ($this->registeredCategories as $categoryName => $sources) { + $categoryTranslations = []; + + // Use the last source (matching Translator's LIFO resolution order) + $source = end($sources); + + foreach ($localesToCheck as $localeToCheck) { + $messages = $source->getMessages($localeToCheck); + + foreach ($messages as $key => $data) { + // Later locale (more specific) overwrites earlier (fallback) + $categoryTranslations[$key] = $data['message']; + } + } + + // Filter out untranslated messages (where translation === source key) + $categoryTranslations = array_filter( + $categoryTranslations, + fn (string $translation, string $key) => $translation !== $key, + ARRAY_FILTER_USE_BOTH, + ); + + if (! empty($categoryTranslations)) { + $result[$categoryName] = $categoryTranslations; + } + } + + return $result; + } + /** * Prepares a source translation to be lazy-translated with [[translate()]]. * @@ -403,12 +466,12 @@ private function defineAppLocales(): void ]); // Add in any extra locales defined by the config - foreach ($this->generalConfig->extraAppLocales as $localeId) { + foreach (Cms::config()->extraAppLocales as $localeId) { $this->appLocaleIds->put($localeId, true); } - if ($this->generalConfig->defaultCpLanguage) { - $this->appLocaleIds->put($this->generalConfig->defaultCpLanguage, true); + if (Cms::config()->defaultCpLanguage) { + $this->appLocaleIds->put(Cms::config()->defaultCpLanguage, true); } } } diff --git a/src/Translation/TranslationServiceProvider.php b/src/Translation/TranslationServiceProvider.php index 9d62eaf59f5..741fad4bd33 100644 --- a/src/Translation/TranslationServiceProvider.php +++ b/src/Translation/TranslationServiceProvider.php @@ -16,32 +16,29 @@ final class TranslationServiceProvider extends ServiceProvider #[\Override] public function register(): void { - $this->app->singleton(function (): Translator { - $translator = new Translator( - locale: app()->getLocale(), - fallbackLocale: $this->app->make(ConfigRepository::class)->get('app.fallback_locale'), - defaultCategory: 'app', - ); + $this->app->singleton(fn (): Translator => new Translator( + locale: app()->getLocale(), + fallbackLocale: $this->app->make(ConfigRepository::class)->get('app.fallback_locale'), + defaultCategory: 'app', + )); + } - $appMessageSource = new MessageSource(dirname(__DIR__, 2).'/resources/translations'); - $formatter = new IntlMessageFormatter; + public function boot(): void + { + $this->callAfterResolving(I18N::class, function (I18N $i18n): void { $appCategory = new CategorySource( name: 'app', - reader: $appMessageSource, - formatter: $formatter + reader: new MessageSource(dirname(__DIR__, 2).'/resources/translations'), + formatter: new IntlMessageFormatter, ); - $siteMessageSource = new MessageSource(lang_path()); - $formatter = new IntlMessageFormatter; $siteCategory = new CategorySource( name: 'site', - reader: $siteMessageSource, - formatter: $formatter + reader: new MessageSource(lang_path()), + formatter: new IntlMessageFormatter, ); - $translator->addCategorySources($appCategory, $siteCategory); - - return $translator; + $i18n->addCategorySources($appCategory, $siteCategory); }); } } diff --git a/src/Utility/Utilities/ProjectConfig.php b/src/Utility/Utilities/ProjectConfig.php index 8e6c143129f..8591dcb418f 100644 --- a/src/Utility/Utilities/ProjectConfig.php +++ b/src/Utility/Utilities/ProjectConfig.php @@ -42,7 +42,6 @@ public static function contentHtml(): string $areChangesPending = $projectConfig->areChangesPending(force: true); if ($areChangesPending) { - $invert = ( ! $projectConfig->readOnly && ! $projectConfig->writeYamlAutomatically && diff --git a/src/View/HtmlStack.php b/src/View/HtmlStack.php index 03a3d167622..5cea1227580 100644 --- a/src/View/HtmlStack.php +++ b/src/View/HtmlStack.php @@ -15,8 +15,6 @@ use Illuminate\Support\Collection; use Stringable; -use function CraftCms\Cms\t; - /** * Manages the registration and rendering of front-end assets (JavaScript, CSS, HTML, meta tags, etc.) * for a single request lifecycle. @@ -211,49 +209,6 @@ public function jsImport(string $key, string $value): void $this->jsImports[$key] = $value; } - /** - * Registers JavaScript translation messages. - * - * For each message whose translation differs from the original, a line of JavaScript is - * registered that populates `Craft.translations[category][message]`. Messages that don't - * have a translation are skipped. - * - * @param array $messages The message strings to translate. - * @param string $category The translation category (e.g. `'app'` or `'site'`). - */ - public function translations(array $messages, string $category = 'app'): void - { - $jsCategory = Json::encode($category); - - $lines = collect($messages) - ->map(function (string $message) use ($jsCategory, $category): ?string { - $translation = t($message, category: $category); - - if ($translation === $message) { - return null; - } - - $jsMessage = Json::encode($message); - $jsTranslation = Json::encode($translation); - - return "Craft.translations[$jsCategory][$jsMessage] = $jsTranslation;"; - }) - ->whereNotNull(); - - if ($lines->isEmpty()) { - return; - } - - $assignments = $lines->implode(PHP_EOL); - - $this->js(<<subDays(1); $endDate = now(); - // The controller adds a day to endDate and starts at day of startDate - // So it covers [startDate->startOfDay(), endDate->addDay()->startOfDay()) - - $count = DB::table(Table::USERS) - ->whereBetween('dateCreated', [ - $startDate->copy()->startOfDay(), - $endDate->copy()->addDay()->startOfDay(), - ]) - ->count(); + // Create users within the date range to assert on a known count + UserModel::factory()->active()->count(3)->create(); postJson(action([NewUsersController::class, 'data']), [ 'startDate' => $startDate->timestamp, 'endDate' => $endDate->timestamp, ])->assertOk() - ->assertJsonPath('total', $count); + ->assertJsonPath('total', 4); // 3 new + 1 admin user from Install }); diff --git a/tests/Unit/Translation/I18NTest.php b/tests/Unit/Translation/I18NTest.php index 2e0646c4992..32293c4b3d2 100644 --- a/tests/Unit/Translation/I18NTest.php +++ b/tests/Unit/Translation/I18NTest.php @@ -213,3 +213,148 @@ expect(I18N::getAppLocaleIds())->toContain('pt-BR'); expect(I18N::validateAppLocaleId('pt-BR'))->toBeTrue(); }); + +test('getAllTranslationsForLocale returns translations for registered categories', function () { + $reader = new class implements \Yiisoft\Translator\MessageReaderInterface + { + public function getMessage(string $id, string $category, string $locale, array $parameters = []): ?string + { + return $this->getMessages($category, $locale)[$id]['message'] ?? null; + } + + public function getMessages(string $category, string $locale): array + { + if ($locale === 'nl') { + return [ + 'Save' => ['message' => 'Opslaan'], + 'Cancel' => ['message' => 'Annuleren'], + ]; + } + + return []; + } + }; + + $source = new \Yiisoft\Translator\CategorySource('testcat', $reader); + I18N::addCategorySources($source); + + $translations = I18N::getAllTranslationsForLocale('nl'); + + expect($translations)->toHaveKey('testcat') + ->and($translations['testcat'])->toBe([ + 'Save' => 'Opslaan', + 'Cancel' => 'Annuleren', + ]); +}); + +test('getAllTranslationsForLocale filters out untranslated messages', function () { + $reader = new class implements \Yiisoft\Translator\MessageReaderInterface + { + public function getMessage(string $id, string $category, string $locale, array $parameters = []): ?string + { + return $this->getMessages($category, $locale)[$id]['message'] ?? null; + } + + public function getMessages(string $category, string $locale): array + { + return [ + 'Translated' => ['message' => 'Vertaald'], + 'Untranslated' => ['message' => 'Untranslated'], + ]; + } + }; + + $source = new \Yiisoft\Translator\CategorySource('filtertest', $reader); + I18N::addCategorySources($source); + + $translations = I18N::getAllTranslationsForLocale('nl'); + + expect($translations['filtertest'])->toHaveKey('Translated') + ->and($translations['filtertest'])->not->toHaveKey('Untranslated'); +}); + +test('getAllTranslationsForLocale excludes categories with no translated messages', function () { + $reader = new class implements \Yiisoft\Translator\MessageReaderInterface + { + public function getMessage(string $id, string $category, string $locale, array $parameters = []): ?string + { + return null; + } + + public function getMessages(string $category, string $locale): array + { + return [ + 'Same' => ['message' => 'Same'], + ]; + } + }; + + $source = new \Yiisoft\Translator\CategorySource('emptycat', $reader); + I18N::addCategorySources($source); + + $translations = I18N::getAllTranslationsForLocale('nl'); + + expect($translations)->not->toHaveKey('emptycat'); +}); + +test('getAllTranslationsForLocale handles locale fallback', function () { + $reader = new class implements \Yiisoft\Translator\MessageReaderInterface + { + public function getMessage(string $id, string $category, string $locale, array $parameters = []): ?string + { + return $this->getMessages($category, $locale)[$id]['message'] ?? null; + } + + public function getMessages(string $category, string $locale): array + { + if ($locale === 'fr') { + return [ + 'Hello' => ['message' => 'Bonjour'], + 'Goodbye' => ['message' => 'Au revoir'], + ]; + } + + if ($locale === 'fr-CA') { + return [ + 'Hello' => ['message' => 'Allô'], + ]; + } + + return []; + } + }; + + $source = new \Yiisoft\Translator\CategorySource('fallbacktest', $reader); + I18N::addCategorySources($source); + + $translations = I18N::getAllTranslationsForLocale('fr-CA'); + + // fr-CA should override fr for 'Hello', but 'Goodbye' should fall through from fr + expect($translations['fallbacktest'])->toBe([ + 'Hello' => 'Allô', + 'Goodbye' => 'Au revoir', + ]); +}); + +test('getAllTranslationsForLocale returns real app translations for Dutch', function () { + $translations = I18N::getAllTranslationsForLocale('nl'); + + expect($translations)->toHaveKey('app') + ->and($translations['app'])->toHaveKey('(blank)') + ->and($translations['app']['(blank)'])->toBe('(leeg)'); +}); + +test('getAllTranslationsForLocale returns few or no app translations for English', function () { + $translations = I18N::getAllTranslationsForLocale('en-US'); + + // en-US falls back to en, which has a few entries that differ from the source key + // (e.g., capitalization fixes, template content). But it should be far fewer than + // a fully translated locale like Dutch. + $dutchTranslations = I18N::getAllTranslationsForLocale('nl'); + + $enCount = isset($translations['app']) ? count($translations['app']) : 0; + $nlCount = count($dutchTranslations['app']); + + expect($enCount)->toBeLessThan(50) + ->and($nlCount)->toBeGreaterThan(500); +}); diff --git a/tests/Unit/View/HtmlStackTest.php b/tests/Unit/View/HtmlStackTest.php index 0e56364f201..1c63bd6d70b 100644 --- a/tests/Unit/View/HtmlStackTest.php +++ b/tests/Unit/View/HtmlStackTest.php @@ -672,25 +672,6 @@ }); }); -describe('translations', function () { - it('does not register JS when no messages have translations', function () { - // When the message equals its translation, nothing should be registered - $this->registry->translations(['untranslated-key-that-will-not-exist'], 'app'); - - expect($this->registry->headHtml())->toBe('') - ->and($this->registry->bodyEndHtml())->toBe(''); - }); - - it('registers translation JS at body position', function () { - // Since translations use t() which may return the same string in test env, - // we verify the position is Body by checking the method doesn't pollute Head - $this->registry->translations(['some-message'], 'app'); - - // Whether or not translations were found, head should not have translation JS - expect($this->registry->headHtml())->not->toContain('Craft.translations'); - }); -}); - describe('icons', function () { it('deduplicates icon registrations', function () { $this->registry->icons(['heart', 'star']); diff --git a/yii2-adapter/legacy/controllers/SystemSettingsController.php b/yii2-adapter/legacy/controllers/SystemSettingsController.php index 5dbde7bc7a4..8cb895bb2ea 100644 --- a/yii2-adapter/legacy/controllers/SystemSettingsController.php +++ b/yii2-adapter/legacy/controllers/SystemSettingsController.php @@ -229,11 +229,6 @@ public function actionGlobalSetIndex(): Response { $view = $this->getView(); $view->registerAssetBundle(AdminTableAsset::class); - $view->registerTranslations('yii2-adapter', [ - 'Global Set Name', - 'No global sets exist yet.', - ]); - return $this->rendertemplate('yii2-adapter/settings/globals/_index', [ 'title' => t('Globals', category: 'yii2-adapter'), 'crumbs' => [ diff --git a/yii2-adapter/legacy/web/View.php b/yii2-adapter/legacy/web/View.php index 8ca3d81d197..45b282ac2c8 100644 --- a/yii2-adapter/legacy/web/View.php +++ b/yii2-adapter/legacy/web/View.php @@ -1438,35 +1438,10 @@ public function getBodyHtml(bool $clear = true): string * * @param string $category The category the messages are in * @param string[] $messages The messages to be translated - * @deprecated 6.0.0 use {@see \CraftCms\Cms\View\HtmlStack::translations()} instead. + * @deprecated 6.0.0 All translations are now loaded in bulk via `window.Craft.translations`. */ public function registerTranslations(string $category, array $messages): void { - $jsCategory = Json::encode($category); - $js = ''; - - foreach ($messages as $message) { - $translation = t($message, category: $category); - - if ($translation !== $message) { - $jsMessage = Json::encode($message); - $jsTranslation = Json::encode($translation); - $js .= ($js !== '' ? PHP_EOL : '') . "Craft.translations[$jsCategory][$jsMessage] = $jsTranslation;"; - } - } - - if ($js === '') { - return; - } - - $js = <<registerJs($js, self::POS_BEGIN); } /** diff --git a/yii2-adapter/legacy/web/assets/admintable/AdminTableAsset.php b/yii2-adapter/legacy/web/assets/admintable/AdminTableAsset.php index 5af852f1fba..5c8d778c652 100644 --- a/yii2-adapter/legacy/web/assets/admintable/AdminTableAsset.php +++ b/yii2-adapter/legacy/web/assets/admintable/AdminTableAsset.php @@ -10,7 +10,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; use craft\web\assets\vue\VueAsset; -use craft\web\View; /** * Asset bundle for admin tables @@ -55,19 +54,4 @@ public function init(): void parent::init(); } - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'item', - 'items', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/authmethodsetup/AuthMethodSetupAsset.php b/yii2-adapter/legacy/web/assets/authmethodsetup/AuthMethodSetupAsset.php index 8a52c64dea0..90ba9394831 100644 --- a/yii2-adapter/legacy/web/assets/authmethodsetup/AuthMethodSetupAsset.php +++ b/yii2-adapter/legacy/web/assets/authmethodsetup/AuthMethodSetupAsset.php @@ -9,7 +9,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; -use craft\web\View; /** * Authentication method setup asset bundle. @@ -43,20 +42,4 @@ class AuthMethodSetupAsset extends AssetBundle public $css = [ 'css/auth.css', ]; - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'Download codes', - 'QR Code', - '{name} added successfully.', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/cp/CpAsset.php b/yii2-adapter/legacy/web/assets/cp/CpAsset.php index 547c5192355..d624ccc4987 100644 --- a/yii2-adapter/legacy/web/assets/cp/CpAsset.php +++ b/yii2-adapter/legacy/web/assets/cp/CpAsset.php @@ -115,7 +115,7 @@ public function registerAssetFiles($view): void parent::registerAssetFiles($view); if ($view instanceof View) { - $this->_registerTranslations(); + $this->_registerIcons(); } // Define the Craft object @@ -126,377 +126,8 @@ public function registerAssetFiles($view): void HtmlStack::js($js, Position::Head); } - private function _registerTranslations(): void + private function _registerIcons(): void { - HtmlStack::translations([ - '(blank)', - 'Characters left: {chars, number}', - 'A server error occurred.', - 'Actions', - 'Add Group', - 'Add', - 'Add…', - 'All', - 'Announcements', - 'Another page already has that name.', - 'Any changes will be lost if you leave this page.', - 'Apply this to the {number} remaining conflicts?', - 'Apply', - 'Are you sure you want to close the editor? Any changes will be lost.', - 'Are you sure you want to close this screen? Any changes will be lost.', - 'Are you sure you want to delete the selected {type}?', - 'Are you sure you want to delete this image?', - 'Are you sure you want to delete this {type}?', - 'Are you sure you want to delete “{name}”?', - 'Are you sure you want to discard your changes?', - 'Are you sure you want to move the selected items?', - 'Are you sure you want to remove the page “{name}”?', - 'Are you sure you want to transfer your license to this domain?', - 'Are you sure you want to undo the move?', - 'Ascending', - 'Assets', - 'Attributes', - 'Back to pages', - 'Back to sources', - 'Breadcrumbs', - 'Buy {name}', - 'Cancel', - 'Change', - 'Changes saved.', - 'Check your email for instructions to reset your password.', - 'Choose a page', - 'Choose a user', - 'Choose which sites this source should be visible for.', - 'Choose which table columns should be visible for this source by default.', - 'Choose which user groups should have access to this source.', - 'Choose', - 'Clear search', - 'Clear', - 'Close Preview', - 'Close', - 'Collapse all blocks', - 'Collapse selected blocks', - 'Color hex value', - 'Color picker', - 'Content', - 'Continue', - 'Copied to clipboard.', - 'Copy URL', - 'Copy all {type}', - 'Copy from', - 'Copy selected {type}', - 'Copy the URL', - 'Copy the reference tag', - 'Copy to clipboard', - 'Copy “{name}” value', - 'Copy', - 'Could not save due to validation errors.', - 'Couldn’t delete “{name}”.', - 'Couldn’t reorder items.', - 'Couldn’t save new order.', - 'Create {type}', - 'Create', - 'Custom', - 'Customize sources', - 'Default Sort', - 'Default Table Columns', - 'Default View Mode', - 'Delete custom source', - 'Delete folder', - 'Delete their content', - 'Delete them', - 'Delete {num, plural, =1{user} other{users}} and content', - 'Delete {num, plural, =1{user} other{users}}', - 'Delete', - 'Descending', - 'Desktop', - 'Device type', - 'Discard changes', - 'Discard', - 'Display as cards', - 'Display as thumbnails', - 'Display in a structured table', - 'Display in a table', - 'Done', - 'Draft Name', - 'Duplicate', - 'Edit draft settings', - 'Edit {type}', - 'Edit', - 'Edited', - 'Element', - 'Elements', - 'Enabled for all sites', - 'Enabled', - 'Enter the name of the folder', - 'Enter your password to log back in.', - 'Error', - 'Existing {type}', - 'Expand all blocks', - 'Expand selected blocks', - 'Export Type', - 'Export', - 'Export…', - 'Failed', - 'Fields', - 'Folder actions', - 'Folder created.', - 'Folder created.', - 'Folder deleted.', - 'Folder deleted.', - 'Folder renamed.', - 'Folder renamed.', - 'Format', - 'Found {num, number} {num, plural, =1{error} other{errors}} in this tab.', - 'From {date}', - 'From', - 'General', - 'Give your tab a name.', - 'Group Name', - 'Handle', - 'Heading', - 'Height unit', - 'Hide sidebar', - 'Hide', - 'Incorrect password.', - 'Indexing assets: {progress}', - 'Information', - 'Instructions', - 'Invalid email.', - 'Invalid username or email.', - 'Items reordered.', - 'Keep both', - 'Keep me signed in', - 'Keep them', - 'Label', - 'Landscape', - 'Level {num}', - 'License transferred.', - 'Limit', - 'Loading complete', - 'Loading', - 'Make not required', - 'Make optional', - 'Make required', - 'Merge the folder (any conflicting files will be replaced)', - 'Missing or empty {items}', - 'Missing {items}', - 'Mobile', - 'More info', - 'More items', - 'More', - 'More…', - 'Move backward', - 'Move down', - 'Move folder', - 'Move forward', - 'Move reverted.', - 'Move to next group', - 'Move to previous group', - 'Move to the left', - 'Move to the right', - 'Move to {page}', - 'Move to', - 'Move up', - 'Move', - 'Name', - 'New child', - 'New custom source', - 'New entry in the {section} section', - 'New entry, choose a section', - 'New field', - 'New file uploaded.', - 'New heading', - 'New order saved.', - 'New page', - 'New position saved.', - 'New position saved.', - 'New subfolder', - 'New {section} entry', - 'New {type}', - 'Next Page', - 'No limit', - 'Notes', - 'Notice', - 'Number of columns', - 'OK', - 'Open in a new tab', - 'Options', - 'Page Name', - 'Page settings', - 'Pages', - 'Password', - 'Past year', - 'Past {num} days', - 'Paste {type}', - 'Pay {price}', - 'Pending', - 'Phone', - 'Portrait', - 'Preview file', - 'Preview', - 'Previewing {type} device in {orientation}', - 'Previewing {type} device', - 'Previous Page', - 'Process complete', - 'Processing', - 'Really delete folder “{folder}”?', - 'Recent Activity', - 'Refresh', - 'Reload', - 'Remove heading', - 'Remove page', - 'Remove {label}', - 'Remove', - 'Rename folder', - 'Rename', - 'Reorder', - 'Replace it', - 'Replace the folder (all existing files will be deleted)', - 'Replace', - 'Required', - 'Rotate', - 'Row could not be added. Maximum number of rows reached.', - 'Row could not be deleted. Minimum number of rows reached.', - 'Row {index}', - 'Save as a new {type}', - 'Save', - 'Saved {timestamp} by {creator}', - 'Saved {timestamp}', - 'Saving', - 'Score', - 'Search in subfolders', - 'Search', - 'Select all', - 'Select element', - 'Select transform', - 'Select {element}', - 'Select', - 'Settings', - 'Show nav', - 'Show nested sources', - 'Show sidebar', - 'Show {title} children', - 'Show', - 'Show/hide children', - 'Showing your unsaved changes.', - 'Showing {first, number}-{last, number} of {total, number} {total, plural, =1{{item}} other{{items}}}', - 'Showing {total, number} {total, plural, =1{{item}} other{{items}}}', - 'Sign out now', - 'Sites', - 'Skip to card view designer', - 'Skip to top of preview', - 'Sort ascending', - 'Sort attribute', - 'Sort by', - 'Sort descending', - 'Sort direction', - 'Source settings saved', - 'Source settings', - 'Sources', - 'Structure', - 'Submit', - 'Success', - 'Switching sites will lose unsaved changes. Are you sure you want to switch sites?', - 'Table Columns', - 'Tablet', - 'The draft could not be saved.', - 'The draft has been saved.', - 'The following {items} could not be found or are empty. Should they be deleted from the index?', - 'The following {items} could not be found. Should they be deleted from the index?', - 'This can be left blank if you just want an unlabeled separator.', - 'This field has been modified.', - 'This month', - 'This tab is conditional', - 'This week', - 'This year', - 'This {type} has been updated.', - 'Tip', - 'Title', - 'To {date}', - 'To', - 'Today', - 'Transfer it to:', - 'Try again', - 'Try another way', - 'Undo', - 'Unread announcements', - 'Update {type}', - 'Upload a file', - 'Upload failed for “{filename}”.', - 'Upload failed.', - 'Upload files', - 'Use defaults', - 'Use the arrow keys to change position, Tab or Spacebar to drop.', - 'User Groups', - 'View in a new tab', - 'View in a new tab', - 'View mode options', - 'View settings', - 'View', - 'Volume path', - 'Warning', - 'What do you want to do with their content?', - 'What do you want to do?', - 'Width unit', - 'You must specify a tab name.', - 'Your changes could not be stored.', - 'Your changes have been stored.', - 'Your session will expire in {time}.', - 'by {creator}', - 'day', - 'days', - 'draft', - 'element', - 'elements', - 'files', - 'folders', - 'hour', - 'hours', - 'minute', - 'minutes', - 'second', - 'seconds', - 'week', - 'weeks', - '{ctrl}C to copy.', - '{element} pagination', - '{first, number}-{last, number} of {total, number} {total, plural, =1{{item}} other{{items}}}', - '{first}-{last} of {total}', - '{item} dropped.', - '{item} picked up.', - '{name} active, more info', - '{name} folder', - '{name} sorted by {attribute}, {direction}', - '{num, number} {num, plural, =1{Available Update} other{Available Updates}}', - '{num, number} {num, plural, =1{degree} other{degrees}}', - '{num, number} {num, plural, =1{notification} other{notifications}}', - '{num, number} {num, plural, =1{result} other{results}}', - '{num} percent complete', - '{pct} width', - '{total, number} {total, plural, =1{error} other{errors}} found in {num, number} {num, plural, =1{tab} other{tabs}}.', - '{total, number} {total, plural, =1{{item}} other{{items}}}', - '{total, number} {type} copied.', - '{totalItems, plural, =1{Item} other{Items}} moved.', - '{type} Criteria', - '{type} copied.', - '{type} saved.', - '“{name}” deleted.', - ]); - - HtmlStack::translations([ - 'New category in the {group} category group', - 'New category, choose a category group', - 'New {group} category', - 'Tag', - ], 'yii2-adapter'); - - HtmlStack::translations([ - '{attribute} cannot be blank.', - '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.', - '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.', - ], 'yii'); - HtmlStack::icons([ 'arrow-down', 'arrow-left', @@ -569,7 +200,7 @@ private function _craftData(): array 'timepickerOptions' => $this->_timepickerOptions($formattingLocale, $orientation), 'timezone' => app()->getTimezone(), 'tokenParam' => $generalConfig->tokenParam, - 'translations' => ['' => ''], // force encode as JS object + 'translations' => I18N::getAllTranslationsForLocale(app()->getLocale()) ?: new \stdClass(), 'useEmailAsUsername' => $generalConfig->useEmailAsUsername, 'usePathInfo' => $generalConfig->usePathInfo, ]; diff --git a/yii2-adapter/legacy/web/assets/craftsupport/CraftSupportAsset.php b/yii2-adapter/legacy/web/assets/craftsupport/CraftSupportAsset.php index 70a7bf89751..05fc4a5dac9 100644 --- a/yii2-adapter/legacy/web/assets/craftsupport/CraftSupportAsset.php +++ b/yii2-adapter/legacy/web/assets/craftsupport/CraftSupportAsset.php @@ -9,7 +9,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; -use craft\web\View; /** * Asset bundle for the Craft Support widget @@ -41,19 +40,4 @@ class CraftSupportAsset extends AssetBundle public $js = [ 'CraftSupportWidget.js', ]; - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'Contact Developer Support', - 'Message sent successfully.', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/dashboard/DashboardAsset.php b/yii2-adapter/legacy/web/assets/dashboard/DashboardAsset.php index 993290c56f6..d233cf399ac 100644 --- a/yii2-adapter/legacy/web/assets/dashboard/DashboardAsset.php +++ b/yii2-adapter/legacy/web/assets/dashboard/DashboardAsset.php @@ -9,7 +9,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; -use craft\web\View; /** * Asset bundle for the Dashboard @@ -41,23 +40,4 @@ class DashboardAsset extends AssetBundle public $js = [ 'Dashboard.js', ]; - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'Couldn’t save widget.', - 'Unable to fetch updates at this time.', - 'Widget saved.', - 'You don’t have any widgets yet.', - '{num, number} {num, plural, =1{column} other{columns}}', - '{type} Settings', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/graphiql/GraphiqlAsset.php b/yii2-adapter/legacy/web/assets/graphiql/GraphiqlAsset.php index eba67b9ad5e..d4cce672dfb 100644 --- a/yii2-adapter/legacy/web/assets/graphiql/GraphiqlAsset.php +++ b/yii2-adapter/legacy/web/assets/graphiql/GraphiqlAsset.php @@ -9,7 +9,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; -use craft\web\View; /** * GraphiQL asset bundle. @@ -36,26 +35,4 @@ class GraphiqlAsset extends AssetBundle public $css = [ 'css/graphiql.css', ]; - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'Explore the GraphQL API', - 'Explorer', - 'History', - 'Prettify query', - 'Prettify', - 'Share query', - 'Share', - 'Toggle explorer', - 'Toggle history', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/matrix/MatrixAsset.php b/yii2-adapter/legacy/web/assets/matrix/MatrixAsset.php index 730752025b1..7f56525150b 100644 --- a/yii2-adapter/legacy/web/assets/matrix/MatrixAsset.php +++ b/yii2-adapter/legacy/web/assets/matrix/MatrixAsset.php @@ -9,7 +9,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; -use craft\web\View; /** * Asset bundle for Matrix fields @@ -34,26 +33,4 @@ class MatrixAsset extends AssetBundle public $js = [ 'MatrixInput.js', ]; - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'Actions', - 'Add an entry', - 'Add {type} above', - 'Collapse', - 'Disable', - 'Disabled', - 'Enable', - 'Entry could not be added. Maximum number of entries reached.', - 'Expand', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/passkeysetup/PasskeySetupAsset.php b/yii2-adapter/legacy/web/assets/passkeysetup/PasskeySetupAsset.php index 3cb7fea0f50..771406e924e 100644 --- a/yii2-adapter/legacy/web/assets/passkeysetup/PasskeySetupAsset.php +++ b/yii2-adapter/legacy/web/assets/passkeysetup/PasskeySetupAsset.php @@ -9,7 +9,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; -use craft\web\View; /** * Passkey setup asset bundle. @@ -36,20 +35,4 @@ class PasskeySetupAsset extends AssetBundle public $js = [ 'PasskeySetup.js', ]; - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'Are you sure you want to delete the “{name}” passkey?', - 'Enter a name for the passkey.', - 'This browser doesn’t support passkeys.', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/plugins/PluginsAsset.php b/yii2-adapter/legacy/web/assets/plugins/PluginsAsset.php index 1600a81f245..0881d255248 100644 --- a/yii2-adapter/legacy/web/assets/plugins/PluginsAsset.php +++ b/yii2-adapter/legacy/web/assets/plugins/PluginsAsset.php @@ -9,7 +9,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; -use craft\web\View; /** * Asset bundle for the Plugins page @@ -41,35 +40,4 @@ class PluginsAsset extends AssetBundle public $js = [ 'PluginManager.js', ]; - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'Renew now for another year of updates.', - 'A license key is required.', - 'Action', - 'Copy package name', - 'Copy plugin handle', - 'Documentation', - 'Install', - 'Missing', - 'Package Name', - 'Plugin Handle', - 'Plugin trials are not allowed on this domain.', - 'Status', - 'Switch', - 'This license is for the {name} edition.', - 'This license is tied to another Craft install. Visit {accountLink} to detach it, or buy a new license', - 'This license isn’t allowed to run version {version}.', - 'Trial', - 'Your license key is invalid.', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/pluginstore/PluginStoreAsset.php b/yii2-adapter/legacy/web/assets/pluginstore/PluginStoreAsset.php index 8787aa4a2a1..fd50fb803ef 100644 --- a/yii2-adapter/legacy/web/assets/pluginstore/PluginStoreAsset.php +++ b/yii2-adapter/legacy/web/assets/pluginstore/PluginStoreAsset.php @@ -10,7 +10,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; use craft\web\assets\vue\VueAsset; -use craft\web\View; /** * Asset bundle for the Plugin Store page @@ -39,122 +38,4 @@ public function init() parent::init(); } - - /** - * @inheritdoc - */ - public function registerAssetFiles($view) - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - '(included)', - '({period} days)', - 'Abandoned', - 'Active Installs', - 'Active Trials', - 'Active trials added to the cart.', - 'Activity', - 'Add all to cart', - 'Add to cart', - 'Added to cart', - 'All reviews', - 'Already in your cart', - 'Ascending', - 'Auto-renew for {price} annually, starting on {date}.', - 'Buy now', - 'Cart', - 'Categories', - 'Changelog', - 'Checkout', - 'Closed Issues', - 'Compatibility', - 'Continue shopping', - 'Couldn’t add all items to the cart.', - 'Couldn’t change Craft CMS edition.', - 'Couldn’t load CMS editions.', - 'Couldn’t load active trials.', - 'Couldn’t update cart’s email.', - 'Couldn’t update item in cart.', - 'Craft CMS edition changed.', - 'Critical', - 'Date Created', - 'Descending', - 'Developer Response', - 'Direction', - 'Discover', - 'Documentation', - 'Edited {updated}', - 'Editions' => 'Editions', - 'Everything in {edition}, plus…', - 'Failed to load plugin reviews. Please try again', - 'For everything else.', - 'For marketing sites managed by small teams.', - 'For personal sites built for yourself or a friend.', - 'Free', - 'Install with Composer', - 'Install', - 'Installation Instructions', - 'Installed as a trial', - 'Installed', - 'Item', - 'Items in your cart', - 'Last Update', - 'Last release', - 'Leave a review', - 'License', - 'Licensed', - 'Loading Plugin Store…', - 'Merged PRs', - 'Name', - 'New Issues', - 'No results.', - 'Only up to {version} is compatible with your version of Craft.', - 'Open PRs', - 'Order by', - 'Overview', - 'Page not found.', - 'Plugin Store', - 'Plugin edition changed.', - 'Plus {renewalPrice}/year for updates after one year.', - 'Popularity', - 'Pricing', - 'Rating', - 'Rating: {rating} out of {max} stars', - 'Reactivate', - 'Remove', - 'Report plugin', - 'Repository', - 'Reviews', - 'Search plugins', - 'See all', - 'Showing results for “{searchQuery}”', - 'The Plugin Store is not available, please try again later.', - 'The developer recommends using {name} instead.', - 'This license is tied to another Craft install. Visit {accountLink} to detach it, or buy a new license.', - 'This plugin doesn’t have any reviews with comments.', - 'This plugin doesn’t have any reviews.', - 'This plugin is no longer maintained.', - 'This plugin isn’t compatible with your version of Craft.', - 'This plugin requires Craft CMS {name} edition.', - 'This plugin requires PHP {v1}, but your composer.json file is currently set to {v2}.', - 'This plugin requires PHP {v1}, but your environment is currently running {v2}.', - 'To install this plugin with composer, copy the command above to your terminal.', - 'Total Price', - 'Total releases', - 'Try for free', - 'Try', - 'Updates until {date}', - 'Updates', - 'Upgrade Craft CMS', - 'Version {version}', - 'Version', - 'Website', - 'Your cart is empty.', - '{num, number} {num, plural, =1{year} other{years}} of updates', - '{totalReviews, plural, =1{# Review} other{# Reviews}}', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/recententries/RecentEntriesAsset.php b/yii2-adapter/legacy/web/assets/recententries/RecentEntriesAsset.php index f1c76d0dcab..bd36ab17d0a 100644 --- a/yii2-adapter/legacy/web/assets/recententries/RecentEntriesAsset.php +++ b/yii2-adapter/legacy/web/assets/recententries/RecentEntriesAsset.php @@ -9,7 +9,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; -use craft\web\View; /** * Asset bundle for Recent Entries widgets @@ -34,18 +33,4 @@ class RecentEntriesAsset extends AssetBundle public $js = [ 'RecentEntriesWidget.js', ]; - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'by {author}', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/systemmessages/SystemMessagesAsset.php b/yii2-adapter/legacy/web/assets/systemmessages/SystemMessagesAsset.php index fc97b313378..9ad4a4ec2fe 100644 --- a/yii2-adapter/legacy/web/assets/systemmessages/SystemMessagesAsset.php +++ b/yii2-adapter/legacy/web/assets/systemmessages/SystemMessagesAsset.php @@ -9,7 +9,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; -use craft\web\View; /** * Asset bundle for the Email Messages page @@ -41,19 +40,4 @@ class SystemMessagesAsset extends AssetBundle public $js = [ 'system_messages.js', ]; - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'Couldn’t save message.', - 'Message saved.', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/updater/UpdaterAsset.php b/yii2-adapter/legacy/web/assets/updater/UpdaterAsset.php index 362a39917fc..f8c0ec197b6 100644 --- a/yii2-adapter/legacy/web/assets/updater/UpdaterAsset.php +++ b/yii2-adapter/legacy/web/assets/updater/UpdaterAsset.php @@ -9,7 +9,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; -use craft\web\View; /** * Asset bundle for the Updater @@ -41,22 +40,4 @@ class UpdaterAsset extends AssetBundle public $js = [ 'Updater.js', ]; - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'A fatal error has occurred:', - 'Status:', - 'Response:', - 'Send for help', - 'Troubleshoot', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/updateswidget/UpdatesWidgetAsset.php b/yii2-adapter/legacy/web/assets/updateswidget/UpdatesWidgetAsset.php index 0131ad82fd4..b89f8f4cee8 100644 --- a/yii2-adapter/legacy/web/assets/updateswidget/UpdatesWidgetAsset.php +++ b/yii2-adapter/legacy/web/assets/updateswidget/UpdatesWidgetAsset.php @@ -9,7 +9,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; -use craft\web\View; /** * Asset bundle for Updates widgets @@ -34,22 +33,4 @@ class UpdatesWidgetAsset extends AssetBundle public $js = [ 'UpdatesWidget.js', ]; - - /** - * @inheritdoc - */ - public function registerAssetFiles($view): void - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'One update available!', - '{total} updates available!', - 'Go to Updates', - 'Congrats! You’re up to date.', - 'Check again', - ]); - } - } } diff --git a/yii2-adapter/legacy/web/assets/upgrade/UpgradeAsset.php b/yii2-adapter/legacy/web/assets/upgrade/UpgradeAsset.php index 9146a2b0c70..17810e4d62b 100644 --- a/yii2-adapter/legacy/web/assets/upgrade/UpgradeAsset.php +++ b/yii2-adapter/legacy/web/assets/upgrade/UpgradeAsset.php @@ -9,7 +9,6 @@ use craft\web\AssetBundle; use craft\web\assets\cp\CpAsset; -use craft\web\View; /** * Asset bundle for the Upgrade utility @@ -43,32 +42,4 @@ class UpgradeAsset extends AssetBundle public $js = [ 'UpgradeUtility.js', ]; - - /** - * @inheritdoc - */ - public function registerAssetFiles($view) - { - parent::registerAssetFiles($view); - - if ($view instanceof View) { - $view->registerTranslations('app', [ - 'All plugins must be compatible with Craft {version} before you can upgrade.', - 'No plugins are installed.', - 'Not installed', - 'Not ready', - 'Plugin', - 'Plugins', - 'Prep {file}', - 'Ready to upgrade?', - 'Ready', - 'Requires PHP {version}', - 'Status', - 'The developer recommends using {name} instead.', - 'Unable to fetch upgrade info at this time.', - 'Unknown', - 'View the upgrade guide', - ]); - } - } } diff --git a/yii2-adapter/src/Yii2ServiceProvider.php b/yii2-adapter/src/Yii2ServiceProvider.php index 628bb3750aa..af1c45bfe45 100644 --- a/yii2-adapter/src/Yii2ServiceProvider.php +++ b/yii2-adapter/src/Yii2ServiceProvider.php @@ -116,6 +116,7 @@ use CraftCms\Cms\Support\Env; use CraftCms\Cms\Support\Facades\Deprecator; use CraftCms\Cms\Support\Facades\Filesystems; +use CraftCms\Cms\Support\Facades\I18N; use CraftCms\Cms\Support\Facades\Twig; use CraftCms\Cms\Support\Str; use CraftCms\Cms\User\Elements\User; @@ -162,7 +163,6 @@ use Yiisoft\Translator\CategorySource; use Yiisoft\Translator\IntlMessageFormatter; use Yiisoft\Translator\Message\Php\MessageSource; -use Yiisoft\Translator\Translator; use function CraftCms\Cms\t; class Yii2ServiceProvider extends ServiceProvider @@ -468,8 +468,7 @@ public function boot(): void if (is_dir(base_path('translations'))) { Deprecator::log('translations-path', 'Storing site translations in `/translations` is deprecated. Rename the folder to `lang` instead.'); - $translator = app(Translator::class); - $translator->addCategorySources(new CategorySource( + I18N::addCategorySources(new CategorySource( 'site', new MessageSource(base_path('translations')), new IntlMessageFormatter(), @@ -479,8 +478,7 @@ public function boot(): void /** * Load legacy translations */ - $translator = app(Translator::class); - $translator->addCategorySources(new CategorySource( + I18N::addCategorySources(new CategorySource( 'yii2-adapter', new MessageSource(dirname(__DIR__) . '/resources/translations'), new IntlMessageFormatter(), diff --git a/yii2-adapter/tests/unit/web/ViewTest.php b/yii2-adapter/tests/unit/web/ViewTest.php index ba9b5fe8143..39ebfd05760 100644 --- a/yii2-adapter/tests/unit/web/ViewTest.php +++ b/yii2-adapter/tests/unit/web/ViewTest.php @@ -7,7 +7,6 @@ namespace crafttests\unit\web; -use Codeception\Stub; use Craft; use craft\test\Craft as CraftTest; use craft\test\mockclasses\arrayable\ExampleArrayable; @@ -17,7 +16,6 @@ use CraftCms\Aliases\Aliases; use CraftCms\Cms\Cms; use CraftCms\Cms\Support\Facades\Sites; -use CraftCms\Cms\Support\Json; use CraftCms\Cms\Twig\TemplateResolver; use CraftCms\Cms\View\Events\RegisterSiteTemplateRoots; use CraftCms\Cms\View\HtmlStack; @@ -235,16 +233,11 @@ public function testTemplateModeException(): void /** * */ - public function testRegisterTranslations(): void + public function testRegisterTranslationsIsNoop(): void { - app()->setLocale('nl'); - - // Basic test that register translations gets rendered - $js = $this->_generateTranslationJs('app', ['Save' => 'Bewaren', 'Cancel' => 'Afbreken']); - $this->_assertRegisterJsInputValues($js, View::POS_BEGIN); + // registerTranslations() is now a no-op — all translations are loaded + // in bulk via window.Craft.translations. Just verify it doesn't throw. $this->view->registerTranslations('app', ['Save', 'Cancel']); - - app()->setLocale('en-US'); } /** @@ -682,42 +675,6 @@ protected function _before(): void } /** - * @param mixed $category - * @param array $messages - * @return string - */ - private function _generateTranslationJs(mixed $category, array $messages): string - { - $category = Json::encode($category); - $js = ''; - foreach ($messages as $message => $translation) { - $translation = Json::encode($translation); - $message = Json::encode($message); - $js .= ($js !== '' ? PHP_EOL : '') . "Craft.translations[$category][$message] = $translation;"; - } - - return "if (typeof Craft.translations[$category] === 'undefined') {" . PHP_EOL . " Craft.translations[$category] = {};" . PHP_EOL . '}' . PHP_EOL . $js; - } - - /** - * @param mixed $desiredJs - * @param mixed $desiredPosition - * @throws \Exception - */ - private function _assertRegisterJsInputValues(mixed $desiredJs, mixed $desiredPosition) - { - $this->view = Stub::construct( - View::class, - [], - [ - 'registerJs' => function($inputJs, $inputPosition) use ($desiredJs, $desiredPosition) { - self::assertSame($desiredJs, $inputJs); - self::assertSame($desiredPosition, $inputPosition); - }, - ] - ); - } - /** * @param string $which * @return array