Просмотр исходного кода

Merge branch 'master' into support/refactor-middlewares

Yuki Takei 5 лет назад
Родитель
Сommit
d46ffc6ad3
90 измененных файлов с 245 добавлено и 150 удалено
  1. 19 1
      .vscode/launch.json
  2. 1 1
      CHANGES.md
  3. 2 0
      config/migrate.js
  4. 0 1
      config/webpack.common.js
  5. 2 2
      package.json
  6. 0 3
      resource/locales/en-US/_conf.json
  7. 0 0
      resource/locales/en_US/admin/admin.json
  8. 0 0
      resource/locales/en_US/admin/userInvitation.txt
  9. 0 0
      resource/locales/en_US/admin/userWaitingActivation.txt
  10. 4 0
      resource/locales/en_US/meta.json
  11. 0 0
      resource/locales/en_US/notifications/comment.txt
  12. 0 0
      resource/locales/en_US/notifications/pageCreate.txt
  13. 0 0
      resource/locales/en_US/notifications/pageDelete.txt
  14. 0 0
      resource/locales/en_US/notifications/pageEdit.txt
  15. 0 0
      resource/locales/en_US/notifications/pageLike.txt
  16. 0 0
      resource/locales/en_US/notifications/pageMove.txt
  17. 0 0
      resource/locales/en_US/sandbox-bootstrap4.md
  18. 0 0
      resource/locales/en_US/sandbox-diagrams.md
  19. 0 0
      resource/locales/en_US/sandbox-math.md
  20. 0 0
      resource/locales/en_US/sandbox.md
  21. 10 10
      resource/locales/en_US/translation.json
  22. 0 0
      resource/locales/en_US/welcome.md
  23. 1 1
      resource/locales/index.js
  24. 0 3
      resource/locales/ja/_conf.json
  25. 0 0
      resource/locales/ja_JP/admin/admin.json
  26. 0 0
      resource/locales/ja_JP/admin/userInvitation.txt
  27. 0 0
      resource/locales/ja_JP/admin/userWaitingActivation.txt
  28. 4 0
      resource/locales/ja_JP/meta.json
  29. 0 0
      resource/locales/ja_JP/notifications/comment.txt
  30. 0 0
      resource/locales/ja_JP/notifications/pageCreate.txt
  31. 0 0
      resource/locales/ja_JP/notifications/pageDelete.txt
  32. 0 0
      resource/locales/ja_JP/notifications/pageEdit.txt
  33. 0 0
      resource/locales/ja_JP/notifications/pageLike.txt
  34. 0 0
      resource/locales/ja_JP/notifications/pageMove.txt
  35. 0 0
      resource/locales/ja_JP/sandbox-bootstrap4.md
  36. 0 0
      resource/locales/ja_JP/sandbox-diagrams.md
  37. 0 0
      resource/locales/ja_JP/sandbox-math.md
  38. 0 0
      resource/locales/ja_JP/sandbox.md
  39. 0 0
      resource/locales/ja_JP/translation.json
  40. 0 0
      resource/locales/ja_JP/welcome.md
  41. 0 3
      resource/locales/zh-CN/_conf.json
  42. 0 0
      resource/locales/zh_CN/admin/admin.json
  43. 0 0
      resource/locales/zh_CN/admin/userInvitation.txt
  44. 0 0
      resource/locales/zh_CN/admin/userWaitingActivation.txt
  45. 4 0
      resource/locales/zh_CN/meta.json
  46. 0 0
      resource/locales/zh_CN/notifications/comment.txt
  47. 0 0
      resource/locales/zh_CN/notifications/pageCreate.txt
  48. 0 0
      resource/locales/zh_CN/notifications/pageDelete.txt
  49. 0 0
      resource/locales/zh_CN/notifications/pageEdit.txt
  50. 0 0
      resource/locales/zh_CN/notifications/pageLike.txt
  51. 0 0
      resource/locales/zh_CN/notifications/pageMove.txt
  52. 0 0
      resource/locales/zh_CN/sandbox-bootstrap4.md
  53. 0 0
      resource/locales/zh_CN/sandbox-diagrams.md
  54. 0 0
      resource/locales/zh_CN/sandbox-math.md
  55. 0 0
      resource/locales/zh_CN/sandbox.md
  56. 0 0
      resource/locales/zh_CN/translation.json
  57. 0 0
      resource/locales/zh_CN/welcome.md
  58. 2 2
      rs-i18n.env
  59. 11 12
      src/client/js/components/Admin/App/AppSetting.jsx
  60. 12 13
      src/client/js/components/InstallerForm.jsx
  61. 11 13
      src/client/js/components/Me/BasicInfoSettings.jsx
  62. 4 1
      src/client/js/components/Me/PasswordSettings.jsx
  63. 10 5
      src/client/js/components/Me/PersonalSettings.jsx
  64. 1 1
      src/client/js/nologin.jsx
  65. 9 9
      src/client/js/services/AppContainer.js
  66. 1 1
      src/client/js/services/PersonalContainer.js
  67. 8 5
      src/client/js/util/i18n.js
  68. 34 0
      src/lib/util/locale-utils.js
  69. 7 0
      src/lib/util/mongoose-utils.js
  70. 47 0
      src/migrations/20200620203632-normalize-locale-id.js
  71. 2 2
      src/server/console.js
  72. 6 7
      src/server/crowi/dev.js
  73. 4 4
      src/server/crowi/express-init.js
  74. 2 7
      src/server/crowi/index.js
  75. 1 1
      src/server/models/config.js
  76. 5 24
      src/server/models/user.js
  77. 3 1
      src/server/routes/apiv3/app-settings.js
  78. 3 1
      src/server/routes/apiv3/personal-setting.js
  79. 1 1
      src/server/routes/apiv3/users.js
  80. 1 1
      src/server/routes/attachment.js
  81. 1 1
      src/server/routes/installer.js
  82. 2 2
      src/server/routes/login-passport.js
  83. 1 1
      src/server/routes/login.js
  84. 1 1
      src/server/routes/user.js
  85. 1 1
      src/server/service/global-notification/index.js
  86. 1 0
      src/server/util/i18nUserSettingDetector.js
  87. 0 2
      src/server/views/layout/layout.html
  88. 2 2
      src/test/global-setup.js
  89. 2 2
      src/test/models/user.test.js
  90. 2 2
      src/test/setup.js

+ 19 - 1
.vscode/launch.json

@@ -29,11 +29,29 @@
         "request": "launch",
         "name": "Debug: Chrome",
         "sourceMaps": true,
-        "webRoot": "${workspaceFolder}/public",
         "sourceMapPathOverrides": {
           "webpack:///*": "${workspaceFolder}/*"
         },
+        "webRoot": "${workspaceFolder}/public",
         "url": "http://localhost:3000"
+      },
+      {
+        "type": "firefox",
+        "request": "launch",
+        "name": "Debug: Firefox",
+        "reAttach": true,
+        "url": "http://localhost:3000",
+        "webRoot": "${workspaceFolder}/public",
+        "pathMappings": [
+          {
+            "url": "webpack:///src",
+            "path": "${workspaceFolder}/src"
+          },
+          {
+            "url": "http://localhost:3000",
+            "path": "${workspaceFolder}/public"
+          }
+        ]
       }
     ]
 }

+ 1 - 1
CHANGES.md

@@ -1357,7 +1357,7 @@ Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/34x.html>
 
 * Fix: The problem that path including round bracket makes something bad
 * Fix: Recursively option processes also unexpedted pages
-* Fix: en-US translation
+* Fix: en_US translation
 
 ## v2.0.7
 

+ 2 - 0
config/migrate.js

@@ -21,6 +21,8 @@ const mongodb = {
   databaseName: url.pathname.substring(1), // omit heading slash
   options: {
     useNewUrlParser: true, // removes a deprecation warning when connecting
+    useUnifiedTopology: true,
+    useFindAndModify: false,
   },
 };
 

+ 0 - 1
config/webpack.common.js

@@ -69,7 +69,6 @@ module.exports = (options) => {
         '@client': helpers.root('src/client'),
         '@tmp': helpers.root('tmp'),
         '@alias/logger': helpers.root('src/lib/service/logger'),
-        '@alias/locales': helpers.root('resource/locales'),
         // replace bunyan
         bunyan: 'browser-bunyan',
       },

+ 2 - 2
package.json

@@ -54,8 +54,8 @@
     "preserver:prod": "npm run migrate",
     "prestart": "npm run build:prod",
     "resource": "node bin/download-cdn-resources.js",
-    "translations": "rs-i18n -lan zh-CN -t",
-    "i18n-json-merge:noTran": "rs-i18n -lan zh-CN",
+    "translations": "rs-i18n -lan zh_CN -t",
+    "i18n-json-merge:noTran": "rs-i18n -lan zh_CN",
     "server:nolazy": "env-cmd -f config/env.dev.js node-dev --nolazy --inspect src/server/app.js",
     "server:dev": "env-cmd -f config/env.dev.js node-dev --inspect src/server/app.js",
     "server:prod:ci": "npm run server:prod -- --ci",

+ 0 - 3
resource/locales/en-US/_conf.json

@@ -1,3 +0,0 @@
-{
-  "name": "English"
-}

+ 0 - 0
resource/locales/en-US/admin/admin.json → resource/locales/en_US/admin/admin.json


+ 0 - 0
resource/locales/en-US/admin/userInvitation.txt → resource/locales/en_US/admin/userInvitation.txt


+ 0 - 0
resource/locales/en-US/admin/userWaitingActivation.txt → resource/locales/en_US/admin/userWaitingActivation.txt


+ 4 - 0
resource/locales/en_US/meta.json

@@ -0,0 +1,4 @@
+{
+  "id": "en_US",
+  "displayName": "English"
+}

+ 0 - 0
resource/locales/en-US/notifications/comment.txt → resource/locales/en_US/notifications/comment.txt


+ 0 - 0
resource/locales/en-US/notifications/pageCreate.txt → resource/locales/en_US/notifications/pageCreate.txt


+ 0 - 0
resource/locales/en-US/notifications/pageDelete.txt → resource/locales/en_US/notifications/pageDelete.txt


+ 0 - 0
resource/locales/en-US/notifications/pageEdit.txt → resource/locales/en_US/notifications/pageEdit.txt


+ 0 - 0
resource/locales/en-US/notifications/pageLike.txt → resource/locales/en_US/notifications/pageLike.txt


+ 0 - 0
resource/locales/en-US/notifications/pageMove.txt → resource/locales/en_US/notifications/pageMove.txt


+ 0 - 0
resource/locales/en-US/sandbox-bootstrap4.md → resource/locales/en_US/sandbox-bootstrap4.md


+ 0 - 0
resource/locales/en-US/sandbox-diagrams.md → resource/locales/en_US/sandbox-diagrams.md


+ 0 - 0
resource/locales/en-US/sandbox-math.md → resource/locales/en_US/sandbox-math.md


+ 0 - 0
resource/locales/en-US/sandbox.md → resource/locales/en_US/sandbox.md


+ 10 - 10
resource/locales/en-US/translation.json → resource/locales/en_US/translation.json

@@ -179,16 +179,16 @@
   },
   "Password": "Password",
   "Password Settings": "Password settings",
-    "personal_settings": {
-    "disassociate_external_account": "Disassociate External Account",
-    "disassociate_external_account_desc": "Are you sure to disassociate the <strong>{{providerType}}</strong> account <strong>{{accountId}}</strong>?",
-    "set_new_password": "Set new Password",
-    "update_password": "Update password",
-      "current_password": "Current password",
-      "new_password": "New password",
-      "new_password_confirm": "Re-enter new password",
-      "password_is_not_set": "Password is not set"
-    },
+  "personal_settings": {
+  "disassociate_external_account": "Disassociate External Account",
+  "disassociate_external_account_desc": "Are you sure to disassociate the <strong>{{providerType}}</strong> account <strong>{{accountId}}</strong>?",
+  "set_new_password": "Set new Password",
+  "update_password": "Update password",
+    "current_password": "Current password",
+    "new_password": "New password",
+    "new_password_confirm": "Re-enter new password",
+    "password_is_not_set": "Password is not set"
+  },
   "security_settings": "Security settings",
   "API Settings": "API settings",
   "API Token Settings": "API token settings",

+ 0 - 0
resource/locales/en-US/welcome.md → resource/locales/en_US/welcome.md


+ 1 - 1
resource/locales/index.js

@@ -1,2 +1,2 @@
-// !!DO NOT REMOVE THIS FILE!!
+// !!DO NOT EDIT/REMOVE THIS FILE!!
 // entry point for @alienfast/i18next-loader

+ 0 - 3
resource/locales/ja/_conf.json

@@ -1,3 +0,0 @@
-{
-  "name": "日本語"
-}

+ 0 - 0
resource/locales/ja/admin/admin.json → resource/locales/ja_JP/admin/admin.json


+ 0 - 0
resource/locales/ja/admin/userInvitation.txt → resource/locales/ja_JP/admin/userInvitation.txt


+ 0 - 0
resource/locales/ja/admin/userWaitingActivation.txt → resource/locales/ja_JP/admin/userWaitingActivation.txt


+ 4 - 0
resource/locales/ja_JP/meta.json

@@ -0,0 +1,4 @@
+{
+  "id": "ja_JP",
+  "displayName": "日本語"
+}

+ 0 - 0
resource/locales/ja/notifications/comment.txt → resource/locales/ja_JP/notifications/comment.txt


+ 0 - 0
resource/locales/ja/notifications/pageCreate.txt → resource/locales/ja_JP/notifications/pageCreate.txt


+ 0 - 0
resource/locales/ja/notifications/pageDelete.txt → resource/locales/ja_JP/notifications/pageDelete.txt


+ 0 - 0
resource/locales/ja/notifications/pageEdit.txt → resource/locales/ja_JP/notifications/pageEdit.txt


+ 0 - 0
resource/locales/ja/notifications/pageLike.txt → resource/locales/ja_JP/notifications/pageLike.txt


+ 0 - 0
resource/locales/ja/notifications/pageMove.txt → resource/locales/ja_JP/notifications/pageMove.txt


+ 0 - 0
resource/locales/ja/sandbox-bootstrap4.md → resource/locales/ja_JP/sandbox-bootstrap4.md


+ 0 - 0
resource/locales/ja/sandbox-diagrams.md → resource/locales/ja_JP/sandbox-diagrams.md


+ 0 - 0
resource/locales/ja/sandbox-math.md → resource/locales/ja_JP/sandbox-math.md


+ 0 - 0
resource/locales/ja/sandbox.md → resource/locales/ja_JP/sandbox.md


+ 0 - 0
resource/locales/ja/translation.json → resource/locales/ja_JP/translation.json


+ 0 - 0
resource/locales/ja/welcome.md → resource/locales/ja_JP/welcome.md


+ 0 - 3
resource/locales/zh-CN/_conf.json

@@ -1,3 +0,0 @@
-{
-  "name": "简体中文"
-}

+ 0 - 0
resource/locales/zh-CN/admin/admin.json → resource/locales/zh_CN/admin/admin.json


+ 0 - 0
resource/locales/zh-CN/admin/userInvitation.txt → resource/locales/zh_CN/admin/userInvitation.txt


+ 0 - 0
resource/locales/zh-CN/admin/userWaitingActivation.txt → resource/locales/zh_CN/admin/userWaitingActivation.txt


+ 4 - 0
resource/locales/zh_CN/meta.json

@@ -0,0 +1,4 @@
+{
+  "id": "zh_CN",
+  "displayName": "简体中文"
+}

+ 0 - 0
resource/locales/zh-CN/notifications/comment.txt → resource/locales/zh_CN/notifications/comment.txt


+ 0 - 0
resource/locales/zh-CN/notifications/pageCreate.txt → resource/locales/zh_CN/notifications/pageCreate.txt


+ 0 - 0
resource/locales/zh-CN/notifications/pageDelete.txt → resource/locales/zh_CN/notifications/pageDelete.txt


+ 0 - 0
resource/locales/zh-CN/notifications/pageEdit.txt → resource/locales/zh_CN/notifications/pageEdit.txt


+ 0 - 0
resource/locales/zh-CN/notifications/pageLike.txt → resource/locales/zh_CN/notifications/pageLike.txt


+ 0 - 0
resource/locales/zh-CN/notifications/pageMove.txt → resource/locales/zh_CN/notifications/pageMove.txt


+ 0 - 0
resource/locales/zh-CN/sandbox-bootstrap4.md → resource/locales/zh_CN/sandbox-bootstrap4.md


+ 0 - 0
resource/locales/zh-CN/sandbox-diagrams.md → resource/locales/zh_CN/sandbox-diagrams.md


+ 0 - 0
resource/locales/zh-CN/sandbox-math.md → resource/locales/zh_CN/sandbox-math.md


+ 0 - 0
resource/locales/zh-CN/sandbox.md → resource/locales/zh_CN/sandbox.md


+ 0 - 0
resource/locales/zh-CN/translation.json → resource/locales/zh_CN/translation.json


+ 0 - 0
resource/locales/zh-CN/welcome.md → resource/locales/zh_CN/welcome.md


+ 2 - 2
rs-i18n.env

@@ -1,9 +1,9 @@
 RS_I18N_BAIDU_APP_ID=
 RS_I18N_BAIDU_APP_SECRET=
 RS_I18N_LOCALES_DIR=./resource/locales
-RS_I18N_BASE_LANGUAGE=en-US
+RS_I18N_BASE_LANGUAGE=en_US
 RS_I18N_TARGET_FILE_SUFFIX= '.json'
 RS_I18N_DEBUG_FORMAT=__{%s}__
 
-RS_I18N_LAN_zh-CN=zh
+RS_I18N_LAN_zh_CN=zh
 RS_I18N_LAN_ja=jp

+ 11 - 12
src/client/js/components/Admin/App/AppSetting.jsx

@@ -3,10 +3,11 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import loggerFactory from '@alias/logger';
 
+import { localeMetadatas } from '../../../util/i18n';
+
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 
-import AppContainer from '../../../services/AppContainer';
 import AdminAppContainer from '../../../services/AdminAppContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
@@ -34,9 +35,8 @@ class AppSetting extends React.Component {
   }
 
   render() {
-    const { t, adminAppContainer, appContainer } = this.props;
-    const locales = appContainer.locales;
-    const languages = Object.keys(locales);
+    const { t, adminAppContainer } = this.props;
+
     return (
       <React.Fragment>
         <div className="form-group row">
@@ -83,20 +83,20 @@ class AppSetting extends React.Component {
           </label>
           <div className="col-md-6">
             {
-              languages.map(lan => (
-                <div key={lan} className="custom-control custom-radio custom-control-inline">
+              localeMetadatas.map(meta => (
+                <div key={meta.id} className="custom-control custom-radio custom-control-inline">
                   <input
                     type="radio"
-                    id={`radioLang${lan}`}
+                    id={`radioLang${meta.id}`}
                     className="custom-control-input"
                     name="globalLang"
-                    value={lan}
-                    checked={adminAppContainer.state.globalLang === lan}
+                    value={meta.id}
+                    checked={adminAppContainer.state.globalLang === meta.id}
                     onChange={(e) => {
                       adminAppContainer.changeGlobalLang(e.target.value);
                     }}
                   />
-                  <label className="custom-control-label" htmlFor={`radioLang${lan}`}>{locales[lan]._conf.name}</label>
+                  <label className="custom-control-label" htmlFor={`radioLang${meta.id}`}>{meta.displayName}</label>
                 </div>
               ))
             }
@@ -145,11 +145,10 @@ class AppSetting extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const AppSettingWrapper = withUnstatedContainers(AppSetting, [AppContainer, AdminAppContainer]);
+const AppSettingWrapper = withUnstatedContainers(AppSetting, [AdminAppContainer]);
 
 AppSetting.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
 };
 

+ 12 - 13
src/client/js/components/InstallerForm.jsx

@@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
 import i18next from 'i18next';
 import { withTranslation } from 'react-i18next';
 
+import { localeMetadatas } from '../util/i18n';
+
 class InstallerForm extends React.Component {
 
   constructor(props) {
@@ -11,13 +13,13 @@ class InstallerForm extends React.Component {
 
     this.state = {
       isValidUserName: true,
-      checkedBtn: 'en-US',
+      checkedBtn: 'en_US',
     };
     this.checkUserName = this.checkUserName.bind(this);
   }
 
   componentWillMount() {
-    this.changeLanguage('en-US');
+    this.changeLanguage('en_US');
   }
 
   checkUserName(event) {
@@ -44,9 +46,6 @@ class InstallerForm extends React.Component {
       : <span><i className="icon-fw icon-ban" />{ this.props.t('installer.unavaliable_user_id') }</span>;
 
     const checkedBtn = this.state.checkedBtn;
-    const { i18n } = this.props;
-    const locales = i18n.options.resources;
-    const languages = Object.keys(locales);
 
     return (
       <div className={`login-dialog p-3 mx-auto${hasErrorClass}`}>
@@ -62,19 +61,19 @@ class InstallerForm extends React.Component {
           <form role="form" action="/installer" method="post" id="register-form" className="col-md-12">
             <div className="form-group text-center">
               {
-                languages.map(lan => (
-                  <div key={lan} className="custom-control custom-radio custom-control-inline">
+                localeMetadatas.map(meta => (
+                  <div key={meta.id} className="custom-control custom-radio custom-control-inline">
                     <input
                       type="radio"
                       className="custom-control-input"
-                      id={`register-form-check-${lan}`}
+                      id={`register-form-check-${meta.id}`}
                       name="registerForm[app:globalLang]"
-                      value={lan}
-                      checked={checkedBtn === lan}
-                      onChange={(e) => { if (e.target.checked) { this.changeLanguage(lan) } }}
+                      value={meta.id}
+                      checked={checkedBtn === meta.id}
+                      onChange={(e) => { if (e.target.checked) { this.changeLanguage(meta.id) } }}
                     />
-                    <label className="custom-control-label" htmlFor={`register-form-check-${lan}`}>
-                      {locales[lan]._conf.name}
+                    <label className="custom-control-label" htmlFor={`register-form-check-${meta.id}`}>
+                      {meta.displayName}
                     </label>
                   </div>
                 ))

+ 11 - 13
src/client/js/components/Me/BasicInfoSettings.jsx

@@ -3,15 +3,16 @@ import React, { Fragment } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
+import { localeMetadatas } from '../../util/i18n';
+
 import { toastSuccess, toastError } from '../../util/apiNotification';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
-import AppContainer from '../../services/AppContainer';
 import PersonalContainer from '../../services/PersonalContainer';
 
 class BasicInfoSettings extends React.Component {
 
-  constructor(appContainer) {
+  constructor() {
     super();
 
     this.onClickSubmit = this.onClickSubmit.bind(this);
@@ -39,10 +40,8 @@ class BasicInfoSettings extends React.Component {
   }
 
   render() {
-    const { t, personalContainer, appContainer } = this.props;
+    const { t, personalContainer } = this.props;
     const { registrationWhiteList } = personalContainer.state;
-    const locales = appContainer.locales;
-    const languages = Object.keys(locales);
 
     return (
       <Fragment>
@@ -113,17 +112,17 @@ class BasicInfoSettings extends React.Component {
           <label className="text-left text-md-right col-md-3 col-form-label">{t('Language')}</label>
           <div className="col-md-6">
             {
-              languages.map(lan => (
-                <div key={lan} className="custom-control custom-radio custom-control-inline">
+              localeMetadatas.map(meta => (
+                <div key={meta.id} className="custom-control custom-radio custom-control-inline">
                   <input
                     type="radio"
-                    id={`radioLang${lan}`}
+                    id={`radioLang${meta.id}`}
                     className="custom-control-input"
                     name="userForm[lang]"
-                    checked={personalContainer.state.lang === lan}
-                    onChange={() => { personalContainer.changeLang(lan) }}
+                    checked={personalContainer.state.lang === meta.id}
+                    onChange={() => { personalContainer.changeLang(meta.id) }}
                   />
-                  <label className="custom-control-label" htmlFor={`radioLang${lan}`}>{locales[lan]._conf.name}</label>
+                  <label className="custom-control-label" htmlFor={`radioLang${meta.id}`}>{meta.displayName}</label>
                 </div>
               ))
             }
@@ -144,11 +143,10 @@ class BasicInfoSettings extends React.Component {
 
 }
 
-const BasicInfoSettingsWrapper = withUnstatedContainers(BasicInfoSettings, [AppContainer, PersonalContainer]);
+const BasicInfoSettingsWrapper = withUnstatedContainers(BasicInfoSettings, [PersonalContainer]);
 
 BasicInfoSettings.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   personalContainer: PropTypes.instanceOf(PersonalContainer).isRequired,
 };
 

+ 4 - 1
src/client/js/components/Me/PasswordSettings.jsx

@@ -67,7 +67,10 @@ class PasswordSettings extends React.Component {
 
     return (
       <React.Fragment>
-        {(!personalContainer.state.isPasswordSet) && <div className="alert alert-warning">{ t('Password is not set') }</div>}
+        { (!personalContainer.state.isPasswordSet) && (
+          <div className="alert alert-warning">{ t('personal_settings.password_is_not_set') }</div>
+        ) }
+
         <div className="container-fluid my-4">
           {(personalContainer.state.isPasswordSet)
             ? <h2 className="border-bottom">{t('personal_settings.update_password')}</h2>

+ 10 - 5
src/client/js/components/Me/PersonalSettings.jsx

@@ -18,19 +18,24 @@ class PersonalSettings extends React.Component {
         <div className="personal-settings">
           <ul className="nav nav-tabs" role="tablist">
             <li className="nav-item">
-              <a className="nav-link active" href="#user-settings" data-toggle="tab" role="tab"><i className="icon-user"></i> { t('User Information') }</a>
+              <a className="nav-link active" href="#user-settings" data-toggle="tab" role="tab">
+                <i className="icon-fw icon-user"></i>{ t('User Information') }
+              </a>
             </li>
             <li className="nav-item">
               <a className="nav-link" href="#external-accounts" data-toggle="tab" role="tab">
-                <i className="icon-share-alt mr-1"></i>
-                { t('admin:user_management.external_accounts') }
+                <i className="icon-fw icon-share-alt"></i>{ t('admin:user_management.external_accounts') }
               </a>
             </li>
             <li className="nav-item">
-              <a className="nav-link" href="#password-settings" data-toggle="tab" role="tab"><i className="icon-lock"></i> { t('Password Settings') }</a>
+              <a className="nav-link" href="#password-settings" data-toggle="tab" role="tab">
+                <i className="icon-fw icon-lock"></i>{ t('Password Settings') }
+              </a>
             </li>
             <li className="nav-item">
-              <a className="nav-link" href="#apiToken" data-toggle="tab" role="tab"><i className="icon-paper-plane"></i> { t('API Settings') }</a>
+              <a className="nav-link" href="#apiToken" data-toggle="tab" role="tab">
+                <i className="icon-fw icon-paper-plane"></i>{ t('API Settings') }
+              </a>
             </li>
           </ul>
           <div className="tab-content p-t-10">

+ 1 - 1
src/client/js/nologin.jsx

@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
 import { Provider } from 'unstated';
 import { I18nextProvider } from 'react-i18next';
 
-import i18nFactory from './util/i18n';
+import { i18nFactory } from './util/i18n';
 
 import AppContainer from './services/AppContainer';
 

+ 9 - 9
src/client/js/services/AppContainer.js

@@ -14,7 +14,7 @@ import {
 } from '../util/color-scheme';
 import Apiv1ErrorHandler from '../util/apiv1ErrorHandler';
 
-import i18nFactory from '../util/i18n';
+import { i18nFactory } from '../util/i18n';
 import apiv3ErrorHandler from '../util/apiv3ErrorHandler';
 
 /**
@@ -42,9 +42,14 @@ export default class AppContainer extends Container {
     const userAgent = window.navigator.userAgent.toLowerCase();
     this.isMobile = /iphone|ipad|android/.test(userAgent);
 
-    const userlang = body.dataset.userlang;
-    this.i18n = i18nFactory(userlang);
-    this.locales = this.i18n.options.resources;
+    const currentUserElem = document.getElementById('growi-current-user');
+    if (currentUserElem != null) {
+      this.currentUser = JSON.parse(currentUserElem.textContent);
+    }
+
+    const userLocaleId = this.currentUser.lang;
+    this.i18n = i18nFactory(userLocaleId);
+
     this.containerInstances = {};
     this.componentInstances = {};
     this.rendererInstances = {};
@@ -79,11 +84,6 @@ export default class AppContainer extends Container {
   initContents() {
     const body = document.querySelector('body');
 
-    const currentUserElem = document.getElementById('growi-current-user');
-    if (currentUserElem != null) {
-      this.currentUser = JSON.parse(currentUserElem.textContent);
-    }
-
     this.isAdmin = body.dataset.isAdmin === 'true';
 
     this.isDocSaved = true;

+ 1 - 1
src/client/js/services/PersonalContainer.js

@@ -24,7 +24,7 @@ export default class PersonalContainer extends Container {
       email: '',
       registrationWhiteList: this.appContainer.getConfig().registrationWhiteList,
       isEmailPublished: false,
-      lang: 'en-US',
+      lang: 'en_US',
       isGravatarEnabled: false,
       isUploadedPicture: false,
       uploadedPictureSrc: this.getUploadedPictureSrc(this.appContainer.currentUser),

+ 8 - 5
src/client/js/util/i18n.js

@@ -2,15 +2,18 @@ import i18n from 'i18next';
 import LanguageDetector from 'i18next-browser-languagedetector';
 import { initReactI18next } from 'react-i18next';
 
-import resources from '@alias/locales';
+import locales from '@root/resource/locales';
 
-export default (userlang) => {
+// extract metadata list from 'resource/locales/${locale}/meta.json'
+export const localeMetadatas = Object.values(locales).map(locale => locale.meta);
+
+export const i18nFactory = (userLocaleId) => {
   // setup LanguageDetector
   const langDetector = new LanguageDetector();
   langDetector.addDetector({
     name: 'userSettingDetector',
     lookup(options) {
-      return userlang;
+      return userLocaleId;
     },
     cacheUserlanguage(lng, options) {
     },
@@ -21,10 +24,10 @@ export default (userlang) => {
     .use(initReactI18next) // if not using I18nextProvider
     .init({
       debug: (process.env.NODE_ENV !== 'production'),
-      resources,
+      resources: locales,
       load: 'currentOnly',
 
-      fallbackLng: 'en-US',
+      fallbackLng: 'en_US',
       detection: {
         order: ['userSettingDetector', 'querystring', 'localStorage'],
       },

+ 34 - 0
src/lib/util/locale-utils.js

@@ -0,0 +1,34 @@
+const fs = require('fs');
+
+const helpers = require('./helpers');
+
+/**
+ * List locales dirents
+ */
+function listLocaleDirents() {
+  const allDirents = fs.readdirSync(helpers.root('resource/locales'), { withFileTypes: true });
+  return allDirents
+    .filter(dirent => dirent.isDirectory());
+}
+
+/**
+ * List locales aliases
+ */
+function listLocaleMetadatas() {
+  return listLocaleDirents()
+    .map(dir => dir.name)
+    .map(localeDirName => require(`../../../resource/locales/${localeDirName}/meta.json`));
+}
+
+/**
+ * List locales IDs (=subdir names)
+ */
+function listLocaleIds() {
+  return listLocaleMetadatas()
+    .map(meta => meta.id);
+}
+
+module.exports = {
+  listLocaleMetadatas,
+  listLocaleIds,
+};

+ 7 - 0
src/lib/util/mongoose-utils.js

@@ -17,7 +17,14 @@ const getModelSafely = (modelName) => {
   return null;
 };
 
+const mongoOptions = {
+  useNewUrlParser: true, // removes a deprecation warning when connecting
+  useUnifiedTopology: true,
+  useFindAndModify: false,
+};
+
 module.exports = {
   getMongoUri,
   getModelSafely,
+  mongoOptions,
 };

+ 47 - 0
src/migrations/20200620203632-normalize-locale-id.js

@@ -0,0 +1,47 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:normalize-locale-id');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+const { getModelSafely } = require('@commons/util/mongoose-utils');
+
+module.exports = {
+  async up(db, client) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Config = getModelSafely('Config') || require('@server/models/config')();
+    const User = getModelSafely('User') || require('@server/models/user')();
+
+    await Promise.all([
+      // update en-US -> en_US
+      Config.update(
+        { key: 'app:globalLang', value: JSON.stringify('en-US') },
+        { value: JSON.stringify('en_US') },
+      ),
+      // update ja -> ja_JP
+      Config.update(
+        { key: 'app:globalLang', value: JSON.stringify('ja') },
+        { value: JSON.stringify('ja_JP') },
+      ),
+
+      // update en-US -> en_US
+      User.updateMany(
+        { lang: 'en-US' },
+        { lang: 'en_US' },
+      ),
+      // update ja -> ja_JP
+      User.updateMany(
+        { lang: 'ja' },
+        { lang: 'ja_JP' },
+      ),
+    ]);
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down(db, client) {
+    // do not rollback
+  },
+};

+ 2 - 2
src/server/console.js

@@ -4,7 +4,7 @@ const repl = require('repl');
 const fs = require('fs');
 const path = require('path');
 const mongoose = require('mongoose');
-const { getMongoUri } = require('@commons/util/mongoose-utils');
+const { getMongoUri, mongoOptions } = require('@commons/util/mongoose-utils');
 
 const models = require('./models');
 
@@ -34,7 +34,7 @@ fs.readFile(replHistoryPath, 'utf8', (err, data) => {
 replServer.context.mongoose = mongoose;
 replServer.context.models = models;
 
-mongoose.connect(getMongoUri(), { useNewUrlParser: true })
+mongoose.connect(getMongoUri(), mongoOptions)
   .then(() => {
     replServer.context.db = mongoose.connection.db;
   });

+ 6 - 7
src/server/crowi/dev.js

@@ -1,10 +1,12 @@
 const logger = require('@alias/logger')('growi:crowi:dev');
-const fs = require('fs');
 const path = require('path');
 
+const { listLocaleIds } = require('@commons/util/locale-utils');
+
 const swig = require('swig-templates');
 const onHeaders = require('on-headers');
 
+
 class CrowiDev {
 
   /**
@@ -38,12 +40,9 @@ class CrowiDev {
    */
   requireForAutoReloadServer() {
     // load all json files for live reloading
-    fs.readdirSync(this.crowi.localeDir)
-      .filter((filename) => {
-        return fs.statSync(path.join(this.crowi.localeDir, filename)).isDirectory();
-      })
-      .forEach((dirname) => {
-        require(path.join(this.crowi.localeDir, dirname, 'translation.json'));
+    listLocaleIds()
+      .forEach((localeId) => {
+        require(path.join(this.crowi.localeDir, localeId, 'translation.json'));
       });
   }
 

+ 4 - 4
src/server/crowi/express-init.js

@@ -20,13 +20,13 @@ module.exports = function(crowi, app) {
 
   const registerSafeRedirect = require('../middlewares/safe-redirect')();
   const injectCurrentuserToLocalvars = require('../middlewares/inject-currentuser-to-localvars')();
+  const { listLocaleIds } = require('@commons/util/locale-utils');
 
   const avoidSessionRoutes = require('../routes/avoid-session-routes');
   const i18nUserSettingDetector = require('../util/i18nUserSettingDetector');
 
   const env = crowi.node_env;
 
-  const User = crowi.model('User');
   const lngDetector = new i18nMiddleware.LanguageDetector();
   lngDetector.addDetector(i18nUserSettingDetector);
 
@@ -36,8 +36,8 @@ module.exports = function(crowi, app) {
     .use(i18nSprintf)
     .init({
       // debug: true,
-      fallbackLng: [User.LANG_EN_US],
-      whitelist: Object.keys(User.getLanguageLabels()).map((k) => { return User[k] }),
+      fallbackLng: ['en_US'],
+      whitelist: listLocaleIds(),
       backend: {
         loadPath: `${crowi.localeDir}{{lng}}/translation.json`,
       },
@@ -70,7 +70,7 @@ module.exports = function(crowi, app) {
     res.locals.consts = {
       pageGrants: Page.getGrantLabels(),
       userStatus: User.getUserStatusLabels(),
-      language:   User.getLanguageLabels(),
+      language:   listLocaleIds(),
       restrictGuestMode: crowi.aclService.getRestrictGuestModeLabels(),
       registrationMode: crowi.aclService.getRegistrationModeLabels(),
     };

+ 2 - 7
src/server/crowi/index.js

@@ -6,9 +6,8 @@ const pkg = require('@root/package.json');
 const InterceptorManager = require('@commons/service/interceptor-manager');
 const CdnResourcesService = require('@commons/service/cdn-resources-service');
 const Xss = require('@commons/service/xss');
-const { getMongoUri } = require('@commons/util/mongoose-utils');
+const { getMongoUri, mongoOptions } = require('@commons/util/mongoose-utils');
 
-const fs = require('fs');
 const path = require('path');
 
 const sep = path.sep;
@@ -33,10 +32,6 @@ function Crowi(rootdir) {
   this.viewsDir = path.join(this.libDir, 'views') + sep;
   this.resourceDir = path.join(this.rootDir, 'resource') + sep;
   this.localeDir = path.join(this.resourceDir, 'locales') + sep;
-  this.locales = fs.readdirSync(this.localeDir)
-    .filter((filename) => {
-      return fs.statSync(path.join(this.localeDir, filename)).isDirectory();
-    });
   this.tmpDir = path.join(this.rootDir, 'tmp') + sep;
   this.cacheDir = path.join(this.tmpDir, 'cache');
 
@@ -202,7 +197,7 @@ Crowi.prototype.setupDatabase = function() {
   // mongoUri = mongodb://user:password@host/dbname
   const mongoUri = getMongoUri();
 
-  return mongoose.connect(mongoUri, { useNewUrlParser: true });
+  return mongoose.connect(mongoUri, mongoOptions);
 };
 
 Crowi.prototype.setupSessionConfig = async function() {

+ 1 - 1
src/server/models/config.js

@@ -38,7 +38,7 @@ module.exports = function(crowi) {
       'app:confidential'  : undefined,
 
       'app:fileUpload'    : false,
-      'app:globalLang'    : 'en-US',
+      'app:globalLang'    : 'en_US',
 
       'security:restrictGuestMode'      : 'Deny',
 

+ 5 - 24
src/server/models/user.js

@@ -11,6 +11,8 @@ const md5 = require('md5');
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const crypto = require('crypto');
 
+const { listLocaleIds } = require('@commons/util/locale-utils');
+
 module.exports = function(crowi) {
   const STATUS_REGISTERED = 1;
   const STATUS_ACTIVE = 2;
@@ -22,11 +24,6 @@ module.exports = function(crowi) {
   /* eslint-disable no-unused-vars */
   const IMAGE_POPULATION = { path: 'imageAttachment', select: 'filePathProxied' };
 
-  const LANG_EN = 'en';
-  const LANG_EN_US = 'en-US';
-  const LANG_EN_GB = 'en-GB';
-  const LANG_JA = 'ja';
-
   const PAGE_ITEMS = 50;
 
   let userEvent;
@@ -58,8 +55,8 @@ module.exports = function(crowi) {
     apiToken: { type: String, index: true },
     lang: {
       type: String,
-      enum: crowi.locales,
-      default: LANG_EN_US,
+      enum: listLocaleIds(),
+      default: 'en_US',
     },
     status: {
       type: Number, required: true, default: STATUS_ACTIVE, index: true,
@@ -146,16 +143,6 @@ module.exports = function(crowi) {
     return hasher.digest('base64');
   }
 
-  function getLanguageLabels() {
-    const lang = {};
-    lang.LANG_EN = LANG_EN;
-    lang.LANG_EN_US = LANG_EN_US;
-    lang.LANG_EN_GB = LANG_EN_GB;
-    lang.LANG_JA = LANG_JA;
-
-    return lang;
-  }
-
   userSchema.methods.isPasswordSet = function() {
     if (this.password) {
       return true;
@@ -354,7 +341,6 @@ module.exports = function(crowi) {
     });
   };
 
-  userSchema.statics.getLanguageLabels = getLanguageLabels;
   userSchema.statics.getUserStatusLabels = function() {
     const userStatus = {};
     userStatus[STATUS_REGISTERED] = 'Approval Pending';
@@ -632,7 +618,7 @@ module.exports = function(crowi) {
         return mailer.send({
           to: user.email,
           subject: `Invitation to ${appTitle}`,
-          template: path.join(crowi.localeDir, 'en-US/admin/userInvitation.txt'),
+          template: path.join(crowi.localeDir, 'en_US/admin/userInvitation.txt'),
           vars: {
             email: user.email,
             password: user.password,
@@ -767,10 +753,5 @@ module.exports = function(crowi) {
   userSchema.statics.IMAGE_POPULATION = IMAGE_POPULATION;
   userSchema.statics.PAGE_ITEMS = PAGE_ITEMS;
 
-  userSchema.statics.LANG_EN = LANG_EN;
-  userSchema.statics.LANG_EN_US = LANG_EN_US;
-  userSchema.statics.LANG_EN_GB = LANG_EN_US;
-  userSchema.statics.LANG_JA = LANG_JA;
-
   return mongoose.model('User', userSchema);
 };

+ 3 - 1
src/server/routes/apiv3/app-settings.js

@@ -6,6 +6,8 @@ const debug = require('debug')('growi:routes:admin');
 
 const express = require('express');
 
+const { listLocaleIds } = require('@commons/util/locale-utils');
+
 const router = express.Router();
 
 const { body } = require('express-validator/check');
@@ -107,7 +109,7 @@ module.exports = (crowi) => {
     appSetting: [
       body('title').trim(),
       body('confidential'),
-      body('globalLang').isIn(crowi.locales),
+      body('globalLang').isIn(listLocaleIds()),
       body('fileUpload').isBoolean(),
     ],
     siteUrlSetting: [

+ 3 - 1
src/server/routes/apiv3/personal-setting.js

@@ -7,6 +7,8 @@ const express = require('express');
 
 const passport = require('passport');
 
+const { listLocaleIds } = require('@commons/util/locale-utils');
+
 const router = express.Router();
 
 const { body } = require('express-validator/check');
@@ -74,7 +76,7 @@ module.exports = (crowi) => {
     personal: [
       body('name').isString().not().isEmpty(),
       body('email').isEmail(),
-      body('lang').isString().isIn(crowi.locales),
+      body('lang').isString().isIn(listLocaleIds()),
       body('isEmailPublished').isBoolean(),
     ],
     imageType: [

+ 1 - 1
src/server/routes/apiv3/users.js

@@ -37,7 +37,7 @@ const validator = {};
  *          lang:
  *            type: string
  *            description: language
- *            example: 'en-US'
+ *            example: 'en_US'
  *          status:
  *            type: integer
  *            description: status

+ 1 - 1
src/server/routes/attachment.js

@@ -215,7 +215,7 @@ module.exports = function(crowi, app) {
   function setHeaderToRes(res, attachment, forceDownload) {
     res.set({
       ETag: `Attachment-${attachment._id}`,
-      'Last-Modified': attachment.createdAt,
+      'Last-Modified': attachment.createdAt.toUTCString(),
     });
 
     // download

+ 1 - 1
src/server/routes/installer.js

@@ -67,7 +67,7 @@ module.exports = function(crowi, app) {
     const username = registerForm.username;
     const email = registerForm.email;
     const password = registerForm.password;
-    const language = registerForm['app:globalLang'] || 'en-US';
+    const language = registerForm['app:globalLang'] || 'en_US';
 
     await appService.initDB(language);
 

+ 2 - 2
src/server/routes/login-passport.js

@@ -253,10 +253,10 @@ module.exports = function(crowi, app) {
     let name;
 
     switch (globalLang) {
-      case 'en-US':
+      case 'en_US':
         name = `${response.name.givenName} ${response.name.familyName}`;
         break;
-      case 'ja':
+      case 'ja_JP':
         name = `${response.name.familyName} ${response.name.givenName}`;
         break;
       default:

+ 1 - 1
src/server/routes/login.js

@@ -161,7 +161,7 @@ module.exports = function(crowi, app) {
       return mailer.send({
         to: admin.email,
         subject: `[${appTitle}:admin] A New User Created and Waiting for Activation`,
-        template: path.join(crowi.localeDir, 'en-US/admin/userWaitingActivation.txt'),
+        template: path.join(crowi.localeDir, 'en_US/admin/userWaitingActivation.txt'),
         vars: {
           createdUser: userData,
           admin,

+ 1 - 1
src/server/routes/user.js

@@ -18,7 +18,7 @@
  *          lang:
  *            type: string
  *            description: language
- *            example: 'en-US'
+ *            example: 'en_US'
  *          status:
  *            type: integer
  *            description: status

+ 1 - 1
src/server/service/global-notification/index.js

@@ -9,7 +9,7 @@ class GlobalNotificationService {
 
   constructor(crowi) {
     this.crowi = crowi;
-    this.defaultLang = 'en-US'; // TODO: get defaultLang from app global config
+    this.defaultLang = 'en_US'; // TODO: get defaultLang from app global config
 
     this.gloabalNotificationMail = new GloabalNotificationMail(crowi);
     this.gloabalNotificationSlack = new GloabalNotificationSlack(crowi);

+ 1 - 0
src/server/util/i18nUserSettingDetector.js

@@ -5,6 +5,7 @@ module.exports = {
     if (req.user == null) {
       return null;
     }
+
     return req.user.lang || null;
   },
 

+ 0 - 2
src/server/views/layout/layout.html

@@ -69,8 +69,6 @@
   data-plugin-enabled="{{ getConfig('crowi', 'plugin:isEnabledPlugins') }}"
   {% block html_base_attr %}{% endblock %}
   data-csrftoken="{{ csrf() }}"
-  data-current-username="{% if user %}{{ user.username }}{% endif %}"
-  data-userlang="{% if user %}{{ user.lang }}{% endif %}"
  >
 
 <div id="wrapper">

+ 2 - 2
src/test/global-setup.js

@@ -7,12 +7,12 @@ if (process.env.NODE_ENV !== 'test') {
 
 const mongoose = require('mongoose');
 
-const { getMongoUri } = require('../lib/util/mongoose-utils');
+const { getMongoUri, mongoOptions } = require('../lib/util/mongoose-utils');
 
 const { getInstance } = require('./setup-crowi');
 
 module.exports = async() => {
-  await mongoose.connect(getMongoUri(), { useNewUrlParser: true });
+  await mongoose.connect(getMongoUri(), mongoOptions);
 
   // drop database
   await mongoose.connection.dropDatabase();

+ 2 - 2
src/test/models/user.test.js

@@ -17,7 +17,7 @@ describe('User', () => {
       username: 'usertest',
       email: 'usertest@example.com',
       password: 'usertestpass',
-      lang: 'en',
+      lang: 'en_US',
     });
 
     done();
@@ -27,7 +27,7 @@ describe('User', () => {
     describe('The user', () => {
       /* eslint-disable jest/no-test-callback */
       test('should created with createUserByEmailAndPassword', (done) => {
-        User.createUserByEmailAndPassword('Example2 for User Test', 'usertest2', 'usertest2@example.com', 'usertest2pass', 'en', (err, userData) => {
+        User.createUserByEmailAndPassword('Example2 for User Test', 'usertest2', 'usertest2@example.com', 'usertest2pass', 'en_US', (err, userData) => {
           expect(err).toBeNull();
           expect(userData).toBeInstanceOf(User);
           expect(userData.name).toBe('Example2 for User Test');

+ 2 - 2
src/test/setup.js

@@ -1,13 +1,13 @@
 const mongoose = require('mongoose');
 
-const { getMongoUri } = require('@commons/util/mongoose-utils');
+const { getMongoUri, mongoOptions } = require('@commons/util/mongoose-utils');
 
 mongoose.Promise = global.Promise;
 
 jest.setTimeout(30000); // default 5000
 
 beforeAll(async(done) => {
-  await mongoose.connect(getMongoUri(), { useNewUrlParser: true });
+  await mongoose.connect(getMongoUri(), mongoOptions);
   done();
 });