Skip to content

Commit 52db7ea

Browse files
committed
Orphaned.
1 parent 9d67733 commit 52db7ea

File tree

6 files changed

+280
-0
lines changed

6 files changed

+280
-0
lines changed

src/Controller/Admin/TranslateStringsController.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,46 @@ public function index() {
7171
$this->set(compact('translateStrings', 'translateDomains'));
7272
}
7373

74+
/**
75+
* Orphaned strings - strings with no references to source code.
76+
*
77+
* @return \Cake\Http\Response|null|void
78+
*/
79+
public function orphaned() {
80+
$projectId = $this->Translation->currentProjectId();
81+
if ($projectId === null) {
82+
$this->Flash->error(__d('translate', 'No project selected.'));
83+
84+
return $this->redirect(['controller' => 'Translate', 'action' => 'index']);
85+
}
86+
87+
$query = $this->TranslateStrings->findOrphaned($projectId);
88+
$count = $query->count();
89+
90+
// Handle bulk actions for ALL orphaned strings
91+
if ($this->request->is('post') && $count > 0) {
92+
$action = $this->request->getData('bulk_action');
93+
$orphanedIds = $this->TranslateStrings->findOrphaned($projectId)->select(['id'])->all()->extract('id')->toArray();
94+
95+
if ($action === 'delete') {
96+
$deleted = $this->TranslateStrings->deleteAll(['id IN' => $orphanedIds]);
97+
$this->Flash->success(__d('translate', '{0} orphaned strings deleted.', $deleted));
98+
99+
return $this->redirect(['action' => 'orphaned']);
100+
}
101+
if ($action === 'deactivate') {
102+
$updated = $this->TranslateStrings->updateAll(['active' => false], ['id IN' => $orphanedIds]);
103+
$this->Flash->success(__d('translate', '{0} orphaned strings marked as inactive.', $updated));
104+
105+
return $this->redirect(['action' => 'orphaned']);
106+
}
107+
}
108+
109+
$translateStrings = $this->paginate($query);
110+
111+
$this->set(compact('translateStrings', 'count'));
112+
}
113+
74114
/**
75115
* View method
76116
*

src/Model/Table/TranslateStringsTable.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,28 @@ public function getUntranslated() {
265265
return $query;
266266
}
267267

268+
/**
269+
* Find orphaned strings (no references to source code).
270+
*
271+
* @param int $projectId Project ID to filter by
272+
* @return \Cake\ORM\Query\SelectQuery
273+
*/
274+
public function findOrphaned(int $projectId): SelectQuery {
275+
return $this->find()
276+
->matching('TranslateDomains', function ($q) use ($projectId) {
277+
return $q->where([
278+
'TranslateDomains.translate_project_id' => $projectId,
279+
]);
280+
})
281+
->where([
282+
'OR' => [
283+
['TranslateStrings.references IS' => null],
284+
['TranslateStrings.references' => ''],
285+
],
286+
])
287+
->orderByDesc('TranslateStrings.modified');
288+
}
289+
268290
/**
269291
* @param int $translateLocaleId
270292
* @param array<\Translate\Model\Entity\TranslateLocale> $translateLocales

templates/Admin/Translate/index.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,11 @@
899899
['controller' => 'TranslateBehavior', 'action' => 'index'],
900900
['escape' => false, 'class' => 'list-group-item list-group-item-action'],
901901
) ?>
902+
<?= $this->Html->link(
903+
'<i class="fas fa-unlink"></i> ' . __d('translate', 'Orphaned Strings'),
904+
['controller' => 'TranslateStrings', 'action' => 'orphaned'],
905+
['escape' => false, 'class' => 'list-group-item list-group-item-action'],
906+
) ?>
902907
<?= $this->Html->link(
903908
'<i class="fas fa-info-circle"></i> ' . __d('translate', 'Best Practices'),
904909
['action' => 'bestPractice'],

templates/Admin/TranslateStrings/index.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
['action' => 'analyze'],
3232
['escape' => false, 'class' => 'list-group-item list-group-item-action'],
3333
) ?>
34+
<?= $this->Html->link(
35+
'<i class="fas fa-unlink"></i> ' . __d('translate', 'Orphaned Strings'),
36+
['action' => 'orphaned'],
37+
['escape' => false, 'class' => 'list-group-item list-group-item-action'],
38+
) ?>
3439
</div>
3540
</div>
3641
</nav>
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<?php
2+
/**
3+
* @var \App\View\AppView $this
4+
* @var iterable<\Translate\Model\Entity\TranslateString> $translateStrings
5+
* @var int $count
6+
*/
7+
8+
use Cake\Core\Plugin;
9+
10+
?>
11+
<div class="row">
12+
<!-- Sidebar -->
13+
<nav class="col-lg-3 col-md-4 mb-4">
14+
<div class="card">
15+
<div class="card-header">
16+
<i class="fas fa-bars"></i> <?= __d('translate', 'Actions') ?>
17+
</div>
18+
<div class="list-group list-group-flush">
19+
<?= $this->Html->link(
20+
'<i class="fas fa-home"></i> ' . __d('translate', 'Overview'),
21+
['controller' => 'Translate', 'action' => 'index'],
22+
['escape' => false, 'class' => 'list-group-item list-group-item-action'],
23+
) ?>
24+
<?= $this->Html->link(
25+
'<i class="fas fa-list"></i> ' . __d('translate', 'All Strings'),
26+
['action' => 'index'],
27+
['escape' => false, 'class' => 'list-group-item list-group-item-action'],
28+
) ?>
29+
</div>
30+
</div>
31+
32+
<div class="card mt-3">
33+
<div class="card-header">
34+
<i class="fas fa-info-circle"></i> <?= __d('translate', 'Info') ?>
35+
</div>
36+
<div class="card-body">
37+
<p class="card-text small text-muted">
38+
<?= __d('translate', 'Orphaned strings are translation strings that no longer have references to source code files. This usually means the original code was removed or refactored.') ?>
39+
</p>
40+
<p class="card-text small text-muted">
41+
<?= __d('translate', 'You can safely delete these strings if they are no longer needed, or keep them if they were manually added.') ?>
42+
</p>
43+
</div>
44+
</div>
45+
</nav>
46+
47+
<!-- Main Content -->
48+
<div class="col-lg-9 col-md-8">
49+
<div class="page-header mb-4">
50+
<h1>
51+
<i class="fas fa-unlink"></i> <?= __d('translate', 'Orphaned Strings') ?>
52+
<span class="badge bg-secondary"><?= $count ?></span>
53+
</h1>
54+
</div>
55+
56+
<?php if ($count > 0) { ?>
57+
<!-- Bulk Actions -->
58+
<?= $this->Form->create(null, ['id' => 'orphaned-form']) ?>
59+
<?= $this->Form->hidden('bulk_action', ['id' => 'bulk-action']) ?>
60+
61+
<div class="card mb-3">
62+
<div class="card-body py-2">
63+
<div class="d-flex justify-content-between align-items-center">
64+
<span class="text-muted">
65+
<i class="fas fa-info-circle"></i>
66+
<?= __d('translate', 'Actions apply to all {0} orphaned strings', $count) ?>
67+
</span>
68+
<div class="btn-group">
69+
<?= $this->Form->button(
70+
'<i class="fas fa-eye-slash"></i> ' . __d('translate', 'Mark All Inactive'),
71+
[
72+
'type' => 'button',
73+
'class' => 'btn btn-warning btn-sm bulk-action-btn',
74+
'escapeTitle' => false,
75+
'data-action' => 'deactivate',
76+
'data-confirm' => __d('translate', 'Mark all {0} orphaned strings as inactive?', $count),
77+
],
78+
) ?>
79+
<?= $this->Form->button(
80+
'<i class="fas fa-trash"></i> ' . __d('translate', 'Delete All'),
81+
[
82+
'type' => 'button',
83+
'class' => 'btn btn-danger btn-sm bulk-action-btn',
84+
'escapeTitle' => false,
85+
'data-action' => 'delete',
86+
'data-confirm' => __d('translate', 'Are you sure you want to delete all {0} orphaned strings? This cannot be undone.', $count),
87+
],
88+
) ?>
89+
</div>
90+
</div>
91+
</div>
92+
</div>
93+
94+
<!-- Results Table -->
95+
<div class="card">
96+
<div class="card-body">
97+
<div class="table-responsive">
98+
<table class="table table-hover align-middle">
99+
<thead>
100+
<tr>
101+
<th><?= $this->Paginator->sort('name'); ?></th>
102+
<th><?= __d('translate', 'Domain') ?></th>
103+
<th class="text-center"><?= $this->Paginator->sort('active') ?></th>
104+
<th><?= $this->Paginator->sort('last_import', null, ['direction' => 'desc']) ?></th>
105+
<th><?= $this->Paginator->sort('modified', null, ['direction' => 'desc']) ?></th>
106+
<th class="text-center"><?= __d('translate', 'Actions') ?></th>
107+
</tr>
108+
</thead>
109+
<tbody>
110+
<?php foreach ($translateStrings as $translateString) { ?>
111+
<tr>
112+
<td>
113+
<span title="<?= h($translateString->name) ?>">
114+
<?= h($this->Text->truncate($translateString->name, 80)) ?>
115+
</span>
116+
<?php if ($translateString->context) { ?>
117+
<br><small class="text-muted">
118+
<i class="fas fa-tag"></i> <?= h($translateString->context) ?>
119+
</small>
120+
<?php } ?>
121+
</td>
122+
<td>
123+
<span class="badge bg-dark">
124+
<i class="fas fa-folder"></i>
125+
<?= h($translateString->_matchingData['TranslateDomains']->name) ?>
126+
</span>
127+
</td>
128+
<td class="text-center">
129+
<?= $this->element('Translate.yes_no', ['value' => $translateString->active]) ?>
130+
</td>
131+
<td><small><?= $this->Time->nice($translateString->last_import) ?></small></td>
132+
<td><small><?= $this->Time->nice($translateString->modified) ?></small></td>
133+
<td class="text-center">
134+
<div class="btn-group btn-group-sm" role="group">
135+
<?= $this->Html->link(
136+
$this->Icon->render('view'),
137+
['action' => 'view', $translateString->id],
138+
['escape' => false, 'class' => 'btn btn-outline-info', 'title' => __d('translate', 'View'), 'data-bs-toggle' => 'tooltip'],
139+
); ?>
140+
<?= $this->Html->link(
141+
$this->Icon->render('edit'),
142+
['action' => 'edit', $translateString->id],
143+
['escape' => false, 'class' => 'btn btn-outline-secondary', 'title' => __d('translate', 'Edit'), 'data-bs-toggle' => 'tooltip'],
144+
); ?>
145+
<?= $this->Form->postLink(
146+
$this->Icon->render('delete'),
147+
['action' => 'delete', $translateString->id],
148+
['escape' => false, 'class' => 'btn btn-outline-danger', 'confirm' => __d('translate', 'Are you sure you want to delete # {0}?', $translateString->id), 'title' => __d('translate', 'Delete'), 'data-bs-toggle' => 'tooltip', 'block' => true],
149+
); ?>
150+
</div>
151+
</td>
152+
</tr>
153+
<?php } ?>
154+
</tbody>
155+
</table>
156+
</div>
157+
</div>
158+
<div class="card-footer">
159+
<?php
160+
if (Plugin::isLoaded('Tools')) {
161+
echo $this->element('Tools.pagination');
162+
} else {
163+
echo $this->element('pagination');
164+
}
165+
?>
166+
</div>
167+
</div>
168+
169+
<?= $this->Form->end() ?>
170+
<?php } else { ?>
171+
<div class="card">
172+
<div class="card-body text-center py-5">
173+
<i class="fas fa-check-circle fa-3x text-success mb-3"></i>
174+
<h4><?= __d('translate', 'No orphaned strings found') ?></h4>
175+
<p class="text-muted"><?= __d('translate', 'All translation strings have references to source code.') ?></p>
176+
</div>
177+
</div>
178+
<?php } ?>
179+
</div>
180+
</div>
181+
182+
<?= $this->fetch('postLink') ?>
183+
184+
<script>
185+
// Handle bulk action buttons
186+
document.querySelectorAll('.bulk-action-btn').forEach(function(button) {
187+
button.addEventListener('click', function(e) {
188+
if (confirm(this.dataset.confirm)) {
189+
document.getElementById('bulk-action').value = this.dataset.action;
190+
document.getElementById('orphaned-form').submit();
191+
}
192+
});
193+
});
194+
</script>

tests/TestCase/Controller/Admin/TranslateStringsControllerTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ public function testIndex() {
4343
$this->assertNoRedirect();
4444
}
4545

46+
/**
47+
* Test orphaned method
48+
*
49+
* @return void
50+
*/
51+
public function testOrphaned() {
52+
$this->disableErrorHandlerMiddleware();
53+
54+
$this->get(['prefix' => 'Admin', 'plugin' => 'Translate', 'controller' => 'TranslateStrings', 'action' => 'orphaned']);
55+
56+
$this->assertResponseCode(200);
57+
$this->assertNoRedirect();
58+
}
59+
4660
/**
4761
* Test view method
4862
*

0 commit comments

Comments
 (0)