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