diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ return array( 'names' => array( - 'core.pkg.css' => '703a28a5', + 'core.pkg.css' => '6c5077ff', 'core.pkg.js' => '5f0169b1', 'darkconsole.pkg.js' => 'ca8671ce', 'differential.pkg.css' => '4a93db37', @@ -104,7 +104,7 @@ 'rsrc/css/application/subscriptions/subscribers-list.css' => '5bb30c78', 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', - 'rsrc/css/core/core.css' => '40151074', + 'rsrc/css/core/core.css' => '2bc4d840', 'rsrc/css/core/remarkup.css' => 'b510c359', 'rsrc/css/core/syntax.css' => '3c18c1cb', 'rsrc/css/core/z-index.css' => 'efb673ac', @@ -469,6 +469,7 @@ 'rsrc/js/core/behavior-toggle-class.js' => 'a82a7769', 'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884', 'rsrc/js/core/behavior-tooltip.js' => '48db4145', + 'rsrc/js/core/behavior-translate.js' => '4eb0011d', 'rsrc/js/core/behavior-watch-anchor.js' => '06e05112', 'rsrc/js/core/behavior-workflow.js' => '0a3f3021', 'rsrc/js/core/phtize.js' => 'd254d646', @@ -633,6 +634,7 @@ 'javelin-behavior-stripe-payment-form' => '1693a296', 'javelin-behavior-test-payment-form' => 'b3e5ee60', 'javelin-behavior-toggle-class' => 'a82a7769', + 'javelin-behavior-translate' => '4eb0011d', 'javelin-behavior-view-placeholder' => '2fa810fc', 'javelin-behavior-workflow' => '0a3f3021', 'javelin-color' => '7e41274a', @@ -690,7 +692,7 @@ 'phabricator-busy' => '6453c869', 'phabricator-chatlog-css' => '852140ff', 'phabricator-content-source-view-css' => '4b8b05d4', - 'phabricator-core-css' => '40151074', + 'phabricator-core-css' => '2bc4d840', 'phabricator-countdown-css' => '86b7b0a0', 'phabricator-crumbs-view-css' => '989a48b6', 'phabricator-dashboard-css' => 'f593f8c2', @@ -1178,6 +1180,13 @@ 1 => 'javelin-dom', 2 => 'javelin-reactor-dom', ), + '4eb0011d' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-stratcom', + 2 => 'javelin-workflow', + 3 => 'javelin-dom', + ), '4f344388' => array( 0 => 'javelin-install', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1210,6 +1210,7 @@ 'PhabricatorApplicationTransactionValueController' => 'applications/transactions/controller/PhabricatorApplicationTransactionValueController.php', 'PhabricatorApplicationTransactionView' => 'applications/transactions/view/PhabricatorApplicationTransactionView.php', 'PhabricatorApplicationTransactions' => 'applications/transactions/application/PhabricatorApplicationTransactions.php', + 'PhabricatorApplicationTranslations' => 'applications/translations/application/PhabricatorApplicationTranslations.php', 'PhabricatorApplicationTypeahead' => 'applications/typeahead/application/PhabricatorApplicationTypeahead.php', 'PhabricatorApplicationUIExamples' => 'applications/uiexample/application/PhabricatorApplicationUIExamples.php', 'PhabricatorApplicationUninstallController' => 'applications/meta/controller/PhabricatorApplicationUninstallController.php', @@ -2156,6 +2157,7 @@ 'PhabricatorSettingsPanelEmailPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php', 'PhabricatorSettingsPanelExternalAccounts' => 'applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php', 'PhabricatorSettingsPanelHomePreferences' => 'applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php', + 'PhabricatorSettingsPanelLanguage' => 'applications/settings/panel/PhabricatorSettingsPanelLanguage.php', 'PhabricatorSettingsPanelMultiFactor' => 'applications/settings/panel/PhabricatorSettingsPanelMultiFactor.php', 'PhabricatorSettingsPanelPassword' => 'applications/settings/panel/PhabricatorSettingsPanelPassword.php', 'PhabricatorSettingsPanelSSHKeys' => 'applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php', @@ -2286,6 +2288,8 @@ 'PhabricatorTransformedFile' => 'applications/files/storage/PhabricatorTransformedFile.php', 'PhabricatorTranslation' => 'infrastructure/internationalization/translation/PhabricatorTranslation.php', 'PhabricatorTranslationsConfigOptions' => 'applications/config/option/PhabricatorTranslationsConfigOptions.php', + 'PhabricatorTranslationsController' => 'applications/translations/controller/PhabricatorTranslationsController.php', + 'PhabricatorTranslationsTranslateController' => 'applications/translations/controller/PhabricatorTranslationsTranslateController.php', 'PhabricatorTrivialTestCase' => 'infrastructure/testing/__tests__/PhabricatorTrivialTestCase.php', 'PhabricatorTwoColumnExample' => 'applications/uiexample/examples/PhabricatorTwoColumnExample.php', 'PhabricatorTypeaheadCommonDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php', @@ -3560,6 +3564,7 @@ 0 => 'HeraldDAO', 1 => 'PhabricatorFlaggableInterface', 2 => 'PhabricatorPolicyInterface', + 3 => 'PhabricatorDestructableInterface', ), 'HeraldRuleController' => 'HeraldController', 'HeraldRuleEdit' => 'HeraldDAO', @@ -3991,6 +3996,7 @@ 'PhabricatorApplicationTransactionValueController' => 'PhabricatorApplicationTransactionController', 'PhabricatorApplicationTransactionView' => 'AphrontView', 'PhabricatorApplicationTransactions' => 'PhabricatorApplication', + 'PhabricatorApplicationTranslations' => 'PhabricatorApplication', 'PhabricatorApplicationTypeahead' => 'PhabricatorApplication', 'PhabricatorApplicationUIExamples' => 'PhabricatorApplication', 'PhabricatorApplicationUninstallController' => 'PhabricatorApplicationsController', @@ -5035,6 +5041,7 @@ 'PhabricatorSettingsPanelEmailPreferences' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelExternalAccounts' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelHomePreferences' => 'PhabricatorSettingsPanel', + 'PhabricatorSettingsPanelLanguage' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelMultiFactor' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelPassword' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelSSHKeys' => 'PhabricatorSettingsPanel', @@ -5164,6 +5171,8 @@ 'PhabricatorTransactionView' => 'AphrontView', 'PhabricatorTransformedFile' => 'PhabricatorFileDAO', 'PhabricatorTranslationsConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorTranslationsController' => 'PhabricatorController', + 'PhabricatorTranslationsTranslateController' => 'PhabricatorTranslationsController', 'PhabricatorTrivialTestCase' => 'PhabricatorTestCase', 'PhabricatorTwoColumnExample' => 'PhabricatorUIExample', 'PhabricatorTypeaheadCommonDatasourceController' => 'PhabricatorTypeaheadDatasourceController', @@ -5342,6 +5351,7 @@ 4 => 'PhabricatorTokenReceiverInterface', 5 => 'PhabricatorFlaggableInterface', 6 => 'PhabricatorApplicationTransactionInterface', + 7 => 'PhabricatorProjectInterface', ), 'PholioMockCommentController' => 'PholioController', 'PholioMockEditController' => 'PholioController', diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php --- a/src/aphront/response/AphrontResponse.php +++ b/src/aphront/response/AphrontResponse.php @@ -59,6 +59,10 @@ } public static function processValueForJSONEncoding(&$value, $key) { + if ($value instanceof PhutilTranslatableText) { + $value = (string)$value; + } + if ($value instanceof PhutilSafeHTMLProducerInterface) { // This renders the producer down to PhutilSafeHTML, which will then // be simplified into a string below. diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -115,6 +115,11 @@ } } + $pref_translate = PhabricatorUserPreferences::PREFERENCE_TRANSLATE_MODE; + if ($preferences->getPreference($pref_translate)) { + PhutilTranslator::getInstance()->setTranslateMode(true); + } + // NOTE: We want to set up the user first so we can render a real page // here, but fire this before any real logic. $restricted = array( diff --git a/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php b/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php --- a/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php +++ b/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php @@ -14,13 +14,13 @@ /** - * Render story text using contextual langauge to identify the object the + * Render story text using contextual language to identify the object the * story is about, instead of the full object name. For example, without * contextual language a story might render like this: * * alincoln created D123: Chop Wood for Log Cabin v2.0 * - * With contextual langauge, it will render like this instead: + * With contextual language, it will render like this instead: * * alincoln created this revision. * diff --git a/src/applications/settings/controller/PhabricatorSettingsMainController.php b/src/applications/settings/controller/PhabricatorSettingsMainController.php --- a/src/applications/settings/controller/PhabricatorSettingsMainController.php +++ b/src/applications/settings/controller/PhabricatorSettingsMainController.php @@ -141,8 +141,8 @@ $group = null; foreach ($panels as $panel) { - if ($panel->getPanelGroup() != $group) { - $group = $panel->getPanelGroup(); + if ((string)$panel->getPanelGroup() != $group) { + $group = (string)$panel->getPanelGroup(); $nav->addLabel($group); } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelAccount.php b/src/applications/settings/panel/PhabricatorSettingsPanelAccount.php --- a/src/applications/settings/panel/PhabricatorSettingsPanelAccount.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelAccount.php @@ -39,9 +39,6 @@ $user->setSex(null); } - // Checked in runtime. - $user->setTranslation($request->getStr('translation')); - $preferences->setPreference($pref_time, $request->getStr($pref_time)); if (!$errors) { @@ -65,22 +62,6 @@ PhutilPerson::SEX_FEMALE => $label_her, ); - $translations = array(); - $symbols = id(new PhutilSymbolLoader()) - ->setType('class') - ->setAncestorClass('PhabricatorTranslation') - ->setConcreteOnly(true) - ->selectAndLoadSymbols(); - foreach ($symbols as $symbol) { - $class = $symbol['name']; - $translations[$class] = newv($class, array())->getName(); - } - asort($translations); - $default = PhabricatorEnv::newObjectFromConfig('translation.provider'); - $translations = array( - '' => pht('Server Default (%s)', $default->getName()), - ) + $translations; - $form = new AphrontFormView(); $form ->setUser($user) @@ -97,12 +78,6 @@ ->setLabel(pht('Pronoun')) ->setName('sex') ->setValue($user->getSex())) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setOptions($translations) - ->setLabel(pht('Translation')) - ->setName('translation') - ->setValue($user->getTranslation())) ->appendRemarkupInstructions( pht( "**Custom Date and Time Formats**\n\n". diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelLanguage.php b/src/applications/settings/panel/PhabricatorSettingsPanelLanguage.php new file mode 100644 --- /dev/null +++ b/src/applications/settings/panel/PhabricatorSettingsPanelLanguage.php @@ -0,0 +1,92 @@ +getUser(); + $username = $user->getUsername(); + + $pref_translate = PhabricatorUserPreferences::PREFERENCE_TRANSLATE_MODE; + $preferences = $user->loadPreferences(); + + $errors = array(); + if ($request->isFormPost()) { + $user->setTranslation($request->getStr('translation')); + + $preferences->setPreference( + $pref_translate, + $request->getBool($pref_translate)); + + if (!$errors) { + $preferences->save(); + $user->save(); + return id(new AphrontRedirectResponse()) + ->setURI($this->getPanelURI('?saved=true')); + } + } + + $translations = array(); + $symbols = id(new PhutilSymbolLoader()) + ->setType('class') + ->setAncestorClass('PhabricatorTranslation') + ->setConcreteOnly(true) + ->selectAndLoadSymbols(); + foreach ($symbols as $symbol) { + $class = $symbol['name']; + $translations[$class] = newv($class, array())->getName(); + } + asort($translations); + $default = PhabricatorEnv::newObjectFromConfig('translation.provider'); + $translations = array( + '' => pht('Server Default (%s)', $default->getName()), + ) + $translations; + + $form = new AphrontFormView(); + $form + ->setUser($user) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setOptions($translations) + ->setLabel(pht('Use Phabricator In')) + ->setName('translation') + ->setValue($user->getTranslation())) + ->appendRemarkupInstructions( + pht( + "WARNING: This feature does not work yet!\n\n". + "IMPORTANT: Enabling this feature may break Phabricator!\n\n". + "In the future, you will be able to help translate Phabricator ". + "to a new language by enabling Translation Mode. However, this ". + "feature does not work yet.")) + ->appendChild( + id(new AphrontFormCheckboxControl()) + ->addCheckbox( + $pref_translate, + 1, + pht('Enable Translation Mode'), + $preferences->getPreference($pref_translate))) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Save Language Settings'))); + + $form_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Account Settings')) + ->setFormSaved($request->getStr('saved')) + ->setFormErrors($errors) + ->setForm($form); + + return $form_box; + } +} diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -29,6 +29,7 @@ const PREFERENCE_DIFF_FILETREE = 'diff-filetree'; const PREFERENCE_CONPH_NOTIFICATIONS = 'conph-notifications'; + const PREFERENCE_TRANSLATE_MODE = 'translate-mode'; protected $userPHID; protected $preferences = array(); diff --git a/src/applications/translations/application/PhabricatorApplicationTranslations.php b/src/applications/translations/application/PhabricatorApplicationTranslations.php new file mode 100644 --- /dev/null +++ b/src/applications/translations/application/PhabricatorApplicationTranslations.php @@ -0,0 +1,33 @@ + array( + 'translate/' => 'PhabricatorTranslationsTranslateController', + ), + ); + } + +} diff --git a/src/applications/translations/controller/PhabricatorTranslationsController.php b/src/applications/translations/controller/PhabricatorTranslationsController.php new file mode 100644 --- /dev/null +++ b/src/applications/translations/controller/PhabricatorTranslationsController.php @@ -0,0 +1,6 @@ +getRequest(); + $viewer = $request->getUser(); + + $spec = phutil_json_decode($request->getStr('spec')); + $original = idx($spec, 'orig'); + $file = idx($spec, 'file'); + $line = idx($spec, 'line'); + + if ($request->isFormPost()) { + $translation = $request->getStr('translation'); + + $response = array( + 'status' => $translation + ? 'user' + : 'none', + ); + return id(new AphrontAjaxResponse())->setContent($response); + } + + $form = id(new AphrontFormView()) + ->setUser($viewer); + + $form + ->addHiddenInput('spec', json_encode($spec)) + ->appendRemarkupInstructions( + pht('WARNING: This feature does not work yet!')) + ->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Source')) + ->setValue($original)); + + if ($file && $line) { + $file = Filesystem::readablePath( + $file, + dirname(phutil_get_library_root('phabricator'))); + + $defined_href = urisprintf( + 'https://secure.phabricator.com/diffusion/P/browse/master/%s$%s', + $file, + $line); + + $form->appendChild( + id(new AphrontFormStaticControl()) + ->setLabel(pht('Defined')) + ->setValue( + phutil_tag( + 'a', + array( + 'href' => $defined_href, + 'target' => '_blank', + ), + basename($file).':'.$line))); + } + + $form->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel(pht('Translation')) + ->setName('translation') + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)); + + return $this->newDialog() + ->setTitle(pht('Translate String')) + ->setWidth(AphrontDialogView::WIDTH_FULL) + ->appendChild($form->buildLayoutView()) + ->addSubmitButton(pht('Save Changes')) + ->addCancelButton('/'); + } + +} diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -91,7 +91,7 @@ $title = $prefix.' '.$title; } - return $title; + return (string)$title; } @@ -153,6 +153,13 @@ Javelin::initBehavior('history-install'); Javelin::initBehavior('phabricator-gesture'); + $prefs = $user->loadPreferences(); + + $pref_translate = PhabricatorUserPreferences::PREFERENCE_TRANSLATE_MODE; + if ($prefs->getPreference($pref_translate)) { + Javelin::initBehavior('translate'); + } + $current_token = null; if ($user) { $current_token = $user->getCSRFToken(); diff --git a/webroot/rsrc/css/core/core.css b/webroot/rsrc/css/core/core.css --- a/webroot/rsrc/css/core/core.css +++ b/webroot/rsrc/css/core/core.css @@ -177,3 +177,21 @@ height: 2px; background: {$sky}; } + +.pht-translatable { + border-style: dashed; + border-width: 0 0 1px; + cursor: alias; +} + +.pht-translation-none { + border-color: {$indigo}; +} + +.pht-translation-file { + border-color: {$sky}; +} + +.pht-translation-user { + border-color: {$green}; +} diff --git a/webroot/rsrc/js/core/behavior-translate.js b/webroot/rsrc/js/core/behavior-translate.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/core/behavior-translate.js @@ -0,0 +1,32 @@ +/** + * @provides javelin-behavior-translate + * @requires javelin-behavior + * javelin-stratcom + * javelin-workflow + * javelin-dom + */ + +JX.behavior('translate', function() { + + JX.Stratcom.listen( + 'click', + 'pht-translatable', + function(e) { + if (!e.getRawEvent().shiftKey) { + return; + } + e.kill(); + + var node = e.getNode('pht-translatable'); + var data = node.getAttribute('data-pht'); + + new JX.Workflow('/translations/translate/', {spec: data}) + .setHandler(function(r) { + JX.DOM.alterClass(node, 'pht-translation-none', (r.status == 'none')); + JX.DOM.alterClass(node, 'pht-translation-file', false); + JX.DOM.alterClass(node, 'pht-translation-user', (r.status == 'user')); + }) + .start(); + }); + +});