@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Moderize Projects

Summary: Add mobile menus, PHTs and clean up panels. There is a litle more to do, but will tackle feed, etc in another diff.

Test Plan: Test Project filters on Chrome and iOS browser.

Reviewers: epriestley, btrahan

Reviewed By: epriestley

CC: aran, Korvin

Differential Revision: https://secure.phabricator.com/D4931

+182 -92
+2 -2
src/applications/project/application/PhabricatorApplicationProject.php
··· 3 3 final class PhabricatorApplicationProject extends PhabricatorApplication { 4 4 5 5 public function getName() { 6 - return 'Projects'; 6 + return pht('Projects'); 7 7 } 8 8 9 9 public function getShortDescription() { 10 - return 'Organize Work'; 10 + return pht('Organize Work'); 11 11 } 12 12 13 13 public function getBaseURI() {
+47 -11
src/applications/project/controller/PhabricatorProjectController.php
··· 5 5 public function buildStandardPageResponse($view, array $data) { 6 6 $page = $this->buildStandardPageView(); 7 7 8 - $page->setApplicationName('Project'); 8 + $page->setApplicationName(pht('Project')); 9 9 $page->setBaseURI('/project/'); 10 10 $page->setTitle(idx($data, 'title')); 11 11 $page->setGlyph("\xE2\x98\xA3"); ··· 30 30 $edit_uri = '/project/edit/'.$id.'/'; 31 31 $members_uri = '/project/members/'.$id.'/'; 32 32 33 - $nav_view->addFilter('dashboard', 'Dashboard'); 34 - $nav_view->addFilter('feed', 'Feed'); 35 - $nav_view->addFilter(null, 'Tasks '.$external_arrow, $tasks_uri); 36 - $nav_view->addFilter(null, 'Wiki '.$external_arrow, $phriction_uri); 37 - $nav_view->addFilter('people', 'People'); 38 - $nav_view->addFilter('about', 'About'); 33 + $nav_view->addFilter('dashboard', pht('Dashboard')); 34 + $nav_view->addFilter('feed', pht('Feed')); 35 + $nav_view->addFilter(null, pht('Tasks').' '.$external_arrow, $tasks_uri); 36 + $nav_view->addFilter(null, pht('Wiki').' '.$external_arrow, $phriction_uri); 37 + $nav_view->addFilter('people', pht('People')); 38 + $nav_view->addFilter('about', pht('About')); 39 39 40 40 $user = $this->getRequest()->getUser(); 41 41 $can_edit = PhabricatorPolicyCapability::CAN_EDIT; 42 42 43 43 if (PhabricatorPolicyFilter::hasCapability($user, $project, $can_edit)) { 44 - $nav_view->addFilter('edit', "Edit Project\xE2\x80\xA6", $edit_uri); 45 - $nav_view->addFilter('members', "Edit Members\xE2\x80\xA6", $members_uri); 44 + $nav_view->addFilter('edit', pht("Edit Project"), $edit_uri); 45 + $nav_view->addFilter('members', pht("Edit Members"), $members_uri); 46 46 } else { 47 47 $nav_view->addFilter( 48 48 'edit', 49 - "Edit Project\xE2\x80\xA6", 49 + pht("Edit Project"), 50 50 $edit_uri, 51 51 $relative = false, 52 52 'disabled'); 53 53 $nav_view->addFilter( 54 54 'members', 55 - "Edit Members\xE2\x80\xA6", 55 + pht("Edit Members"), 56 56 $members_uri, 57 57 $relative = false, 58 58 'disabled'); 59 59 } 60 60 61 61 return $nav_view; 62 + } 63 + 64 + public function buildApplicationMenu() { 65 + return $this->buildSideNavView(null, true)->getMenu(); 66 + } 67 + 68 + public function buildSideNavView($filter = null, $for_app = false) { 69 + $user = $this->getRequest()->getUser(); 70 + 71 + $nav = new AphrontSideNavFilterView(); 72 + $nav 73 + ->setBaseURI(new PhutilURI('/project/filter/')) 74 + ->addLabel(pht('User')) 75 + ->addFilter('active', pht('Active')) 76 + ->addLabel(pht('All')) 77 + ->addFilter('all', pht('All Projects')) 78 + ->addFilter('allactive', pht('Active Projects')) 79 + ->selectFilter($filter, 'active'); 80 + 81 + if ($for_app) { 82 + $nav->addFilter('create/', pht('Create Project')); 83 + } 84 + 85 + return $nav; 86 + } 87 + 88 + public function buildApplicationCrumbs() { 89 + $crumbs = parent::buildApplicationCrumbs(); 90 + 91 + $crumbs->addAction( 92 + id(new PhabricatorMenuItemView()) 93 + ->setName(pht('Create Project')) 94 + ->setHref($this->getApplicationURI('create/')) 95 + ->setIcon('create')); 96 + 97 + return $crumbs; 62 98 } 63 99 64 100 }
+19 -9
src/applications/project/controller/PhabricatorProjectCreateController.php
··· 63 63 $error_view = null; 64 64 if ($errors) { 65 65 $error_view = new AphrontErrorView(); 66 - $error_view->setTitle('Form Errors'); 66 + $error_view->setTitle(pht('Form Errors')); 67 67 $error_view->setErrors($errors); 68 68 } 69 69 ··· 77 77 $form 78 78 ->appendChild( 79 79 id(new AphrontFormTextControl()) 80 - ->setLabel('Name') 80 + ->setLabel(pht('Name')) 81 81 ->setName('name') 82 82 ->setValue($project->getName()) 83 83 ->setError($e_name)) 84 84 ->appendChild( 85 85 id(new AphrontFormTextAreaControl()) 86 - ->setLabel('Blurb') 86 + ->setLabel(pht('Blurb')) 87 87 ->setName('blurb') 88 88 ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) 89 89 ->setValue($profile->getBlurb())); ··· 92 92 $dialog = id(new AphrontDialogView()) 93 93 ->setUser($user) 94 94 ->setWidth(AphrontDialogView::WIDTH_FORM) 95 - ->setTitle('Create a New Project') 95 + ->setTitle(pht('Create a New Project')) 96 96 ->appendChild($error_view) 97 97 ->appendChild($form) 98 - ->addSubmitButton('Create Project') 98 + ->addSubmitButton(pht('Create Project')) 99 99 ->addCancelButton('/project/'); 100 100 101 101 return id(new AphrontDialogResponse())->setDialog($dialog); ··· 104 104 $form 105 105 ->appendChild( 106 106 id(new AphrontFormSubmitControl()) 107 - ->setValue('Create') 107 + ->setValue(pht('Create')) 108 108 ->addCancelButton('/project/')); 109 109 110 110 $panel = new AphrontPanelView(); 111 111 $panel 112 112 ->setWidth(AphrontPanelView::WIDTH_FORM) 113 - ->setHeader('Create a New Project') 113 + ->setHeader(pht('Create a New Project')) 114 + ->setNoBackground() 114 115 ->appendChild($form); 115 116 116 - return $this->buildStandardPageResponse( 117 + $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); 118 + $crumbs->addCrumb( 119 + id(new PhabricatorCrumbView()) 120 + ->setName(pht('Create Project')) 121 + ->setHref($this->getApplicationURI().'create/') 122 + ); 123 + 124 + return $this->buildApplicationPage( 117 125 array( 126 + $crumbs, 118 127 $error_view, 119 128 $panel, 120 129 ), 121 130 array( 122 - 'title' => 'Create new Project', 131 + 'title' => pht('Create New Project'), 132 + 'device' => true 123 133 )); 124 134 } 125 135 }
+24 -24
src/applications/project/controller/PhabricatorProjectListController.php
··· 12 12 public function processRequest() { 13 13 $request = $this->getRequest(); 14 14 15 - $nav = new AphrontSideNavFilterView(); 16 - $nav 17 - ->setBaseURI(new PhutilURI('/project/filter/')) 18 - ->addLabel('User') 19 - ->addFilter('active', 'Active') 20 - ->addLabel('All') 21 - ->addFilter('all', 'All Projects') 22 - ->addFilter('allactive','Active Projects'); 15 + $nav = $this->buildSideNavView($this->filter); 23 16 $this->filter = $nav->selectFilter($this->filter, 'active'); 24 17 25 18 $pager = new AphrontPagerView(); ··· 33 26 34 27 $view_phid = $request->getUser()->getPHID(); 35 28 36 - $status_filter = PhabricatorProjectQuery::STATUS_ANY; 37 - 38 29 switch ($this->filter) { 39 30 case 'active': 40 - $table_header = 'Your Projects'; 31 + $table_header = pht('Your Projects'); 41 32 $query->withMemberPHIDs(array($view_phid)); 42 33 $query->withStatus(PhabricatorProjectQuery::STATUS_ACTIVE); 43 34 break; 44 35 case 'allactive': 45 - $status_filter = PhabricatorProjectQuery::STATUS_ACTIVE; 46 - $table_header = 'Active Projects'; 47 - // fallthrough 36 + $table_header = pht('Active Projects'); 37 + $query->withStatus(PhabricatorProjectQuery::STATUS_ACTIVE); 38 + break; 48 39 case 'all': 49 - $table_header = 'All Projects'; 50 - $query->withStatus($status_filter); 40 + $table_header = pht('All Projects'); 41 + $query->withStatus(PhabricatorProjectQuery::STATUS_ANY); 51 42 break; 52 43 } 53 44 ··· 122 113 $table = new AphrontTableView($rows); 123 114 $table->setHeaders( 124 115 array( 125 - 'Project', 126 - 'Status', 127 - 'Description', 128 - 'Population', 129 - 'Open Tasks', 116 + pht('Project'), 117 + pht('Status'), 118 + pht('Description'), 119 + pht('Population'), 120 + pht('Open Tasks'), 130 121 )); 131 122 $table->setColumnClasses( 132 123 array( ··· 139 130 140 131 $panel = new AphrontPanelView(); 141 132 $panel->setHeader($table_header); 142 - $panel->setCreateButton('Create New Project', '/project/create/'); 143 133 $panel->appendChild($table); 134 + $panel->setNoBackground(); 144 135 $panel->appendChild($pager); 145 136 146 137 $nav->appendChild($panel); 147 138 148 - return $this->buildStandardPageResponse( 139 + $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); 140 + $crumbs->addCrumb( 141 + id(new PhabricatorCrumbView()) 142 + ->setName($table_header) 143 + ->setHref($this->getApplicationURI()) 144 + ); 145 + $nav->setCrumbs($crumbs); 146 + 147 + return $this->buildApplicationPage( 149 148 $nav, 150 149 array( 151 - 'title' => 'Projects', 150 + 'title' => pht('Projects'), 151 + 'device' => true, 152 152 )); 153 153 } 154 154 }
+21 -6
src/applications/project/controller/PhabricatorProjectMembersEditController.php
··· 83 83 ); 84 84 } 85 85 86 - $header_name = 'Edit Members'; 87 - $title = 'Edit Members'; 86 + $header_name = pht('Edit Members'); 87 + $title = pht('Edit Members'); 88 88 89 89 $list = $this->renderMemberList($handles); 90 90 ··· 94 94 ->appendChild( 95 95 id(new AphrontFormTokenizerControl()) 96 96 ->setName('phids') 97 - ->setLabel('Add Members') 97 + ->setLabel(pht('Add Members')) 98 98 ->setDatasource('/typeahead/common/users/')) 99 99 ->appendChild( 100 100 id(new AphrontFormSubmitControl()) 101 101 ->addCancelButton('/project/view/'.$project->getID().'/') 102 - ->setValue('Add Members')); 102 + ->setValue(pht('Add Members'))); 103 103 $faux_form = id(new AphrontFormLayoutView()) 104 104 ->setBackgroundShading(true) 105 105 ->setPadded(true) 106 106 ->appendChild( 107 107 id(new AphrontFormInsetView()) 108 - ->setTitle('Current Members ('.count($handles).')') 108 + ->setTitle(pht('Current Members (%d)', count($handles))) 109 109 ->appendChild($list)); 110 110 111 111 $panel = new AphrontPanelView(); 112 112 $panel->setHeader($header_name); 113 113 $panel->setWidth(AphrontPanelView::WIDTH_FORM); 114 + $panel->setNoBackground(); 114 115 $panel->appendChild($form); 115 116 $panel->appendChild('<br />'); 116 117 $panel->appendChild($faux_form); ··· 119 120 $nav->selectFilter('members'); 120 121 $nav->appendChild($panel); 121 122 122 - return $this->buildStandardPageResponse( 123 + $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); 124 + $crumbs->addCrumb( 125 + id(new PhabricatorCrumbView()) 126 + ->setName($project->getName()) 127 + ->setHref('/project/view/'.$project->getID().'/') 128 + ); 129 + $crumbs->addCrumb( 130 + id(new PhabricatorCrumbView()) 131 + ->setName(pht('Edit Members')) 132 + ->setHref($this->getApplicationURI()) 133 + ); 134 + $nav->setCrumbs($crumbs); 135 + 136 + return $this->buildApplicationPage( 123 137 $nav, 124 138 array( 125 139 'title' => $title, 140 + 'device' => true, 126 141 )); 127 142 } 128 143
+24 -14
src/applications/project/controller/PhabricatorProjectProfileController.php
··· 5 5 6 6 private $id; 7 7 private $page; 8 + private $project; 8 9 9 10 public function willProcessRequest(array $data) { 10 11 $this->id = idx($data, 'id'); ··· 24 25 } 25 26 26 27 $project = $query->executeOne(); 28 + $this->project = $project; 27 29 if (!$project) { 28 30 return new Aphront404Response(); 29 31 } ··· 96 98 array( 97 99 'class' => $class, 98 100 ), 99 - 'Join Project')); 101 + pht('Join Project'))); 100 102 } else { 101 103 $action = javelin_tag( 102 104 'a', ··· 105 107 'sigil' => 'workflow', 106 108 'class' => 'grey button', 107 109 ), 108 - 'Leave Project...'); 110 + pht('Leave Project...')); 109 111 } 110 112 111 113 $header->addAction($action); ··· 115 117 $content = '<div style="padding: 1em;">'.$content.'</div>'; 116 118 $header->appendChild($content); 117 119 118 - return $this->buildStandardPageResponse( 120 + return $this->buildApplicationPage( 119 121 $nav_view, 120 122 array( 121 - 'title' => $project->getName().' Project', 123 + 'title' => pht('%s Project', $project->getName()), 122 124 )); 123 125 } 124 126 ··· 143 145 <div class="phabricator-profile-info-pane"> 144 146 <table class="phabricator-profile-info-table"> 145 147 <tr> 146 - <th>Creator</th> 148 + <th>%s</th> 147 149 <td>%s</td> 148 150 </tr> 149 151 <tr> 150 - <th>Created</th> 152 + <th>%s</th> 151 153 <td>%s</td> 152 154 </tr> 153 155 <tr> ··· 155 157 <td>%s</td> 156 158 </tr> 157 159 <tr> 158 - <th>Blurb</th> 160 + <th>%s</th> 159 161 <td>%s</td> 160 162 </tr> 161 163 </table> 162 164 </div> 163 165 </div>', 166 + pht('Creator'), 164 167 $handles[$project->getAuthorPHID()]->renderLink(), 168 + pht('Created'), 165 169 $timestamp, 166 170 $project->getPHID(), 171 + pht('Blurb'), 167 172 $blurb); 168 173 169 174 return $about; ··· 184 189 if ($affiliated) { 185 190 $affiliated = '<ul>'.implode("\n", $affiliated).'</ul>'; 186 191 } else { 187 - $affiliated = '<p><em>No one is affiliated with this project.</em></p>'; 192 + $affiliated = 193 + '<p><em>'. 194 + pht('No one is affiliated with this project.'). 195 + '</em></p>'; 188 196 } 189 197 190 198 return 191 199 '<div class="phabricator-profile-info-group">'. 192 - '<h1 class="phabricator-profile-info-header">People</h1>'. 200 + '<h1 class="phabricator-profile-info-header">'.pht('People').'</h1>'. 193 201 '<div class="phabricator-profile-info-pane">'. 194 202 $affiliated. 195 203 '</div>'. ··· 207 215 $stories = $query->execute(); 208 216 209 217 if (!$stories) { 210 - return 'There are no stories about this project.'; 218 + return pht('There are no stories about this project.'); 211 219 } 212 220 213 221 return $this->renderStories($stories); ··· 222 230 223 231 return 224 232 '<div class="phabricator-profile-info-group">'. 225 - '<h1 class="phabricator-profile-info-header">Activity Feed</h1>'. 233 + '<h1 class="phabricator-profile-info-header">'. 234 + pht('Activity Feed'). 235 + '</h1>'. 226 236 '<div class="phabricator-profile-info-pane">'. 227 237 $view->render(). 228 238 '</div>'. ··· 257 267 } 258 268 259 269 if (empty($tasks)) { 260 - $task_views = '<em>No open tasks.</em>'; 270 + $task_views = '<em>'.pht('No open tasks.').'</em>'; 261 271 } else { 262 272 $task_views = implode('', $task_views); 263 273 } ··· 269 279 array( 270 280 'href' => '/maniphest/view/all/?projects='.$project->getPHID(), 271 281 ), 272 - "View All Open Tasks \xC2\xBB"); 282 + pht("View All Open Tasks \xC2\xBB")); 273 283 274 284 $content = 275 285 '<div class="phabricator-profile-info-group"> 276 286 <h1 class="phabricator-profile-info-header">'. 277 - "Open Tasks ({$open})". 287 + pht("Open Tasks (%d)", $open). 278 288 '</h1>'. 279 289 '<div class="phabricator-profile-info-pane">'. 280 290 $task_views.
+37 -21
src/applications/project/controller/PhabricatorProjectProfileEditController.php
··· 86 86 $profile->setBlurb($request->getStr('blurb')); 87 87 88 88 if (!strlen($project->getName())) { 89 - $e_name = 'Required'; 90 - $errors[] = 'Project name is required.'; 89 + $e_name = pht('Required'); 90 + $errors[] = pht('Project name is required.'); 91 91 } else { 92 92 $e_name = null; 93 93 } ··· 112 112 $y = 50); 113 113 $profile->setProfileImagePHID($xformed->getPHID()); 114 114 } else { 115 - $e_image = 'Not Supported'; 115 + $e_image = pht('Not Supported'); 116 116 $errors[] = 117 - 'This server only supports these image formats: '. 117 + pht('This server only supports these image formats:').' '. 118 118 implode(', ', $supported_formats).'.'; 119 119 } 120 120 } ··· 132 132 $error_view = null; 133 133 if ($errors) { 134 134 $error_view = new AphrontErrorView(); 135 - $error_view->setTitle('Form Errors'); 135 + $error_view->setTitle(pht('Form Errors')); 136 136 $error_view->setErrors($errors); 137 137 } 138 138 139 - $header_name = 'Edit Project'; 140 - $title = 'Edit Project'; 139 + $header_name = pht('Edit Project'); 140 + $title = pht('Edit Project'); 141 141 $action = '/project/edit/'.$project->getID().'/'; 142 142 143 143 $policies = id(new PhabricatorPolicyQuery()) ··· 153 153 ->setEncType('multipart/form-data') 154 154 ->appendChild( 155 155 id(new AphrontFormTextControl()) 156 - ->setLabel('Name') 156 + ->setLabel(pht('Name')) 157 157 ->setName('name') 158 158 ->setValue($project->getName()) 159 159 ->setError($e_name)) 160 160 ->appendChild( 161 161 id(new AphrontFormSelectControl()) 162 - ->setLabel('Project Status') 162 + ->setLabel(pht('Project Status')) 163 163 ->setName('status') 164 164 ->setOptions($options) 165 165 ->setValue($project->getStatus())) 166 166 ->appendChild( 167 167 id(new AphrontFormTextAreaControl()) 168 - ->setLabel('Blurb') 168 + ->setLabel(pht('Blurb')) 169 169 ->setName('blurb') 170 170 ->setValue($profile->getBlurb())) 171 - ->appendChild(hsprintf( 172 - '<p class="aphront-form-instructions">NOTE: Policy settings are not '. 173 - 'yet fully implemented. Some interfaces still ignore these settings, '. 174 - 'particularly "Visible To".</p>')) 171 + ->appendChild('<p class="aphront-form-instructions">'. 172 + pht('NOTE: Policy settings are not yet fully implemented. '. 173 + 'Some interfaces still ignore these settings, '. 174 + 'particularly "Visible To".').'</p>') 175 175 ->appendChild( 176 176 id(new AphrontFormPolicyControl()) 177 177 ->setUser($user) 178 178 ->setName('can_view') 179 - ->setCaption('Members can always view a project.') 179 + ->setCaption(pht('Members can always view a project.')) 180 180 ->setPolicyObject($project) 181 181 ->setPolicies($policies) 182 182 ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ··· 192 192 ->setUser($user) 193 193 ->setName('can_join') 194 194 ->setCaption( 195 - 'Users who can edit a project can always join a project.') 195 + pht('Users who can edit a project can always join a project.')) 196 196 ->setPolicyObject($project) 197 197 ->setPolicies($policies) 198 198 ->setCapability(PhabricatorPolicyCapability::CAN_JOIN)) 199 199 ->appendChild( 200 200 id(new AphrontFormMarkupControl()) 201 - ->setLabel('Profile Image') 201 + ->setLabel(pht('Profile Image')) 202 202 ->setValue( 203 203 phutil_tag( 204 204 'img', ··· 207 207 )))) 208 208 ->appendChild( 209 209 id(new AphrontFormImageControl()) 210 - ->setLabel('Change Image') 210 + ->setLabel(pht('Change Image')) 211 211 ->setName('image') 212 212 ->setError($e_image) 213 - ->setCaption('Supported formats: '.implode(', ', $supported_formats))) 213 + ->setCaption( 214 + pht('Supported formats:').' '.implode(', ', $supported_formats))) 214 215 ->appendChild( 215 216 id(new AphrontFormSubmitControl()) 216 217 ->addCancelButton('/project/view/'.$project->getID().'/') 217 - ->setValue('Save')); 218 + ->setValue(pht('Save'))); 218 219 219 220 $panel = new AphrontPanelView(); 220 221 $panel->setHeader($header_name); 221 222 $panel->setWidth(AphrontPanelView::WIDTH_FORM); 223 + $panel->setNoBackground(); 222 224 $panel->appendChild($form); 223 225 224 226 $nav = $this->buildLocalNavigation($project); ··· 229 231 $panel, 230 232 )); 231 233 232 - return $this->buildStandardPageResponse( 234 + $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); 235 + $crumbs->addCrumb( 236 + id(new PhabricatorCrumbView()) 237 + ->setName($project->getName()) 238 + ->setHref('/project/view/'.$project->getID().'/') 239 + ); 240 + $crumbs->addCrumb( 241 + id(new PhabricatorCrumbView()) 242 + ->setName(pht('Edit Project')) 243 + ->setHref($this->getApplicationURI()) 244 + ); 245 + $nav->setCrumbs($crumbs); 246 + 247 + return $this->buildApplicationPage( 233 248 $nav, 234 249 array( 235 250 'title' => $title, 251 + 'device' => true, 236 252 )); 237 253 } 238 254 }
+6 -4
src/applications/project/controller/PhabricatorProjectUpdateController.php
··· 61 61 case 'leave': 62 62 $dialog = new AphrontDialogView(); 63 63 $dialog->setUser($user); 64 - $dialog->setTitle('Really leave project?'); 64 + $dialog->setTitle(pht('Really leave project?')); 65 65 $dialog->appendChild( 66 - '<p>Your tremendous contributions to this project will be sorely '. 67 - 'missed. Are you sure you want to leave?</p>'); 66 + '<p>'. 67 + pht('Your tremendous contributions to this project will be sorely '. 68 + 'missed. Are you sure you want to leave?'). 69 + '</p>'); 68 70 $dialog->addCancelButton($project_uri); 69 - $dialog->addSubmitButton('Leave Project'); 71 + $dialog->addSubmitButton(pht('Leave Project')); 70 72 break; 71 73 default: 72 74 return new Aphront404Response();
+2 -1
src/applications/project/editor/PhabricatorProjectEditor.php
··· 164 164 165 165 if ($slug == '/') { 166 166 throw new PhabricatorProjectNameCollisionException( 167 - "Project names must be unique and contain some letters or numbers."); 167 + pht("Project names must be unique and contain some ". 168 + "letters or numbers.")); 168 169 } 169 170 170 171 $id = $project->getID();