Merged PR 6: タスク 1484: 言語切り替えの仕組みをいれる

## 概要
[タスク 1484: 言語切り替えの仕組みをいれる](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/OMDSDictation/_workitems/edit/1484)

- トップ画面での言語切り替え機能を実装しました。
  - 英語、ドイツ語、フランス語、スペイン語で切り替えできるようにしています。

## レビューポイント
- 言語切り替えとして機能に不足はないか
- デザインは仮組なので対象外
  - コンボボックスで言語切り替えできるところのみ確認をお願いします。

## UIの変更
- 言語切り替え追加
  - [Task1484](https://ndstokyo.sharepoint.com/:f:/r/sites/Piranha/Shared%20Documents/General/OMDS/%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/Task1484?csf=1&web=1&e=e3lu7p)

## 動作確認状況
- 画面上で言語切り替えできることを確認

## 補足
- デザインはタグだけの仮組ですので無視してください。
This commit is contained in:
makabe.t 2023-03-09 08:46:31 +00:00
parent 7329591b6f
commit 8822ddaee4
16 changed files with 280 additions and 1451 deletions

File diff suppressed because it is too large Load Diff

View File

@ -51,7 +51,7 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.6", "@babel/core": "^7.18.6",
"@mdx-js/react": "^2.1.2", "@mdx-js/react": "^2.1.2",
"@openapitools/openapi-generator-cli": "^2.5.1", "@openapitools/openapi-generator-cli": "^0.0.6",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/luxon": "^3.2.0", "@types/luxon": "^3.2.0",
"@types/react": "^18.0.0", "@types/react": "^18.0.0",

View File

@ -8,10 +8,12 @@ import { useEffect } from "react";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import globalAxios, { AxiosError, AxiosResponse } from "axios"; import globalAxios, { AxiosError, AxiosResponse } from "axios";
import { clearToken } from "features/auth"; import { clearToken } from "features/auth";
import { useTranslation } from "react-i18next";
const App = (): JSX.Element => { const App = (): JSX.Element => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { instance } = useMsal(); const { instance } = useMsal();
const [t, i18n] = useTranslation();
const pca = new PublicClientApplication(msalConfig); const pca = new PublicClientApplication(msalConfig);
useEffect(() => { useEffect(() => {
const id = globalAxios.interceptors.response.use( const id = globalAxios.interceptors.response.use(
@ -32,6 +34,18 @@ const App = (): JSX.Element => {
return cleanup; return cleanup;
}, [dispatch, instance]); }, [dispatch, instance]);
// Language読み取り
useEffect(() => {
const language = document.cookie
.split(";")
.map((x) => x.split("="))
.find((x) => x.length === 2 && x[0] === "language");
if (language) {
i18n.changeLanguage(language[1]);
}
}, [i18n]);
return ( return (
<> <>
<GlobalStyle /> <GlobalStyle />

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 362.41 42" style="enable-background:new 0 0 362.41 42;" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_00000106112683229958979730000004837138376666885508_);}
.st1{clip-path:url(#SVGID_00000100361991382501253840000013201318416462760885_);}
.st2{clip-path:url(#SVGID_00000100361991382501253840000013201318416462760885_);fill-rule:evenodd;clip-rule:evenodd;}
</style>
<g id="OM_System_Black_-_One_Line_-_RGB_00000168110105817326191910000010615373263908845735_">
<g>
<defs>
<rect id="SVGID_1_" y="0" width="362.41" height="42"/>
</defs>
<clipPath id="SVGID_00000097489272735381870970000015716550008865822343_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000097489272735381870970000015716550008865822343_);">
<defs>
<rect id="SVGID_00000084515526535015927220000011441361349608841099_" y="0" width="362.41" height="42"/>
</defs>
<clipPath id="SVGID_00000005241618409923234440000016758378329632926897_">
<use xlink:href="#SVGID_00000084515526535015927220000011441361349608841099_" style="overflow:visible;"/>
</clipPath>
<path style="clip-path:url(#SVGID_00000005241618409923234440000016758378329632926897_);" d="M62.35,23.65l-9.06-22.2H41.24
v15.26C39.28,7.17,30.89,0,20.84,0C9.33,0,0,9.4,0,21c0,11.59,9.33,21,20.84,21c10.05,0,18.44-7.17,20.4-16.71v15.07h9.45V19.18
l8.07,21.18h7.19l8.07-21.18v21.18h9.45V1.45H71.42L62.35,23.65z M20.84,32.57C14.5,32.57,9.36,27.39,9.36,21
S14.5,9.43,20.84,9.43S32.33,14.61,32.33,21S27.19,32.57,20.84,32.57"/>
<path style="clip-path:url(#SVGID_00000005241618409923234440000016758378329632926897_);fill-rule:evenodd;clip-rule:evenodd;" d="
M127.89,17.47h9.94c0,0,0-2.32-0.27-3.9c-0.27-1.58-0.84-3.09-2.45-4.37c-1.62-1.27-5.01-1.81-6.65-2.11
c-1.65-0.3-7.16-0.4-10.55-0.4s-8.27,0.29-10.37,0.73c-2.06,0.43-4.88,1.77-5.96,3.5c-0.75,1.18-1.85,3.52-1.85,6.13
c0,2.65,0.36,5.14,1.09,6.45c0.71,1.26,1.41,2.03,2.58,2.7c1.18,0.67,3.44,1.01,6.32,1.14c2.85,0.13,6.22,0.19,8.93,0.27
c2.75,0.09,6.41,0.25,7.89,0.36c1.63,0.12,2.35,1.15,2.35,2.19c0,1.05-0.5,1.83-1.54,2.19c-1.16,0.39-5.29,0.35-8.01,0.38
c-2.65,0.04-6.03-0.14-7.64-0.32c-1.34-0.15-2.16-1.23-2.32-2.77h-9.85c0,0,0.02,2.8,0.32,4.34c0.3,1.54,1.01,2.72,1.85,3.66
c0.84,0.94,1.68,1.41,3.05,1.85c1.38,0.44,7.76,0.87,13.5,0.87c5.74,0,10.58-0.07,12.43-0.37c1.84-0.31,3.86-0.98,4.93-1.82
c1.08-0.84,2.2-2.25,2.84-3.76c0.48-1.12,0.72-2.99,0.69-4.83c-0.04-1.85-0.57-4.87-1.54-6.18c-0.98-1.31-1.58-2.08-3.12-2.66
c-1.55-0.57-4.16-0.91-5.81-0.97c-1.64-0.07-6.95-0.23-9.54-0.3c-2.11-0.05-6.6-0.24-7.62-0.47c-0.93-0.21-1.54-0.64-1.54-1.92
c0-1.27,0.68-1.73,1.44-1.91c0.99-0.24,3.56-0.66,7.19-0.66c3.63,0,6.35,0.25,7.12,0.42c0.77,0.17,1.38,0.57,1.75,1.04
C127.82,16.46,127.89,17.47,127.89,17.47"/>
<polygon style="clip-path:url(#SVGID_00000005241618409923234440000016758378329632926897_);" points="161.22,19.5 171.82,6.96
184.35,6.96 166.25,28.53 166.25,40.07 156.35,40.07 156.18,28.53 138.08,6.96 150.6,6.96 "/>
<polygon style="clip-path:url(#SVGID_00000005241618409923234440000016758378329632926897_);" points="264.47,6.96 264.47,15.68
249.99,15.68 249.99,40.06 239.92,40.06 239.92,15.68 225.45,15.68 225.45,6.96 "/>
<polygon style="clip-path:url(#SVGID_00000005241618409923234440000016758378329632926897_);" points="278.2,32.47 303.38,32.47
303.38,40.07 278.2,40.07 268.12,40.07 268.12,6.96 278.2,6.96 303.38,6.96 303.38,14.54 278.2,14.54 278.2,19.71 301.84,19.71
301.84,27.31 278.2,27.31 "/>
<path style="clip-path:url(#SVGID_00000005241618409923234440000016758378329632926897_);" d="M335.48,40.06h-3.85l-12.17-21.95
c-0.11-0.21-0.28-0.5-0.53-0.45c-0.31,0.07-0.31,0.43-0.31,0.68v21.73h-10.08V6.96h15.72l11.09,20c0.03,0.06,0.06,0.09,0.12,0.08
c0.05,0.01,0.09-0.02,0.12-0.08l11.09-20h15.72v33.11h-10.07V18.34c0-0.25,0-0.61-0.31-0.68c-0.24-0.05-0.41,0.25-0.52,0.45
l-12.17,21.95H335.48z"/>
<path style="clip-path:url(#SVGID_00000005241618409923234440000016758378329632926897_);fill-rule:evenodd;clip-rule:evenodd;" d="
M212.27,17.47h9.94c0,0,0-2.32-0.27-3.9c-0.27-1.58-0.84-3.09-2.45-4.37c-1.61-1.27-5.01-1.81-6.65-2.11
c-1.65-0.3-7.15-0.4-10.54-0.4c-3.39,0-8.27,0.29-10.37,0.73c-2.06,0.43-4.88,1.77-5.97,3.5c-0.75,1.18-1.85,3.52-1.85,6.13
c0,2.65,0.36,5.14,1.09,6.45c0.71,1.26,1.41,2.03,2.58,2.7c1.18,0.67,3.44,1.01,6.32,1.14c2.85,0.13,6.22,0.19,8.93,0.27
c2.76,0.09,6.42,0.25,7.9,0.36c1.63,0.12,2.35,1.15,2.35,2.19c0,1.05-0.51,1.83-1.55,2.19c-1.15,0.39-5.29,0.35-8.01,0.38
c-2.65,0.04-6.03-0.14-7.64-0.32c-1.33-0.15-2.17-1.23-2.32-2.77h-9.85c0,0,0.02,2.8,0.32,4.34c0.3,1.54,1.01,2.72,1.85,3.66
c0.84,0.94,1.68,1.41,3.05,1.85c1.38,0.44,7.76,0.87,13.5,0.87c5.74,0,10.57-0.07,12.42-0.37c1.84-0.31,3.86-0.98,4.93-1.82
c1.08-0.84,2.2-2.25,2.84-3.76c0.48-1.12,0.72-2.99,0.69-4.83c-0.04-1.85-0.58-4.87-1.54-6.18c-0.98-1.31-1.58-2.08-3.12-2.66
c-1.55-0.57-4.16-0.91-5.81-0.97c-1.64-0.07-6.95-0.23-9.54-0.3c-2.11-0.05-6.6-0.24-7.62-0.47c-0.93-0.21-1.55-0.64-1.55-1.92
c0-1.27,0.69-1.73,1.45-1.91c0.99-0.24,3.56-0.66,7.19-0.66c3.63,0,6.35,0.25,7.12,0.42c0.77,0.17,1.38,0.57,1.75,1.04
C212.2,16.46,212.27,17.47,212.27,17.47"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_レイヤー_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<defs>
<style>.cls-1{fill:#fff;}</style>
</defs>
<path class="cls-1" d="m16,32l-2.1-2.1,12.4-12.4H0v-3h26.2L13.9,2.1l2.1-2.1,16,16-16,16Z"/>
</svg>

After

Width:  |  Height:  |  Size: 277 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_レイヤー_1" data-name="レイヤー 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<defs>
<style>
.cls-1 {
fill: #004086;
}
</style>
</defs>
<path class="cls-1" d="m16,32l-2.1-2.1,12.4-12.4H0v-3h26.2L13.9,2.1l2.1-2.1,16,16-16,16Z"/>
</svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@ -6,9 +6,9 @@ import { updateTokenAsync } from "features/auth/operations";
import { loadAccessToken } from "features/auth/utils"; import { loadAccessToken } from "features/auth/utils";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
//アクセストークンを更新する基準の秒数 // アクセストークンを更新する基準の秒数
const TOKEN_UPDATE_TIME = 5 * 60; const TOKEN_UPDATE_TIME = 5 * 60;
//アクセストークンの更新チェックを行う間隔(ミリ秒) // アクセストークンの更新チェックを行う間隔(ミリ秒)
const TOKEN_UPDATE_INTERVAL_MS = 3 * 60 * 1000; const TOKEN_UPDATE_INTERVAL_MS = 3 * 60 * 1000;
export const UpdateTokenTimer = () => { export const UpdateTokenTimer = () => {

View File

@ -0,0 +1,8 @@
import { getTranslationID } from "../../translation";
export const LANGUAGE_LIST = [
{ value: "en", label: getTranslationID("topPage.label.languageEnglish") },
{ value: "de", label: getTranslationID("topPage.label.languageGerman") },
{ value: "fr", label: getTranslationID("topPage.label.languageFrench") },
{ value: "es", label: getTranslationID("topPage.label.languageSpanish") },
];

View File

@ -0,0 +1,29 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en from "./translation/en.json";
import es from "./translation/es.json";
import fr from "./translation/fr.json";
import de from "./translation/de.json";
i18n.use(initReactI18next).init({
resources: {
en: {
translation: en,
},
de: {
translation: de,
},
fr: {
translation: fr,
},
es: {
translation: es,
},
},
lng: "en",
defaultNS: "translation",
interpolation: {
escapeValue: false,
},
});
export default i18n;

View File

@ -1,9 +1,11 @@
import { store } from "app/store"; import { store } from "app/store";
import React from "react"; import React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { I18nextProvider } from "react-i18next";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import App from "./App"; import App from "./App";
import * as serviceWorker from "./serviceWorker"; import * as serviceWorker from "./serviceWorker";
import i18n from "./i18n";
const container = document.getElementById("root"); const container = document.getElementById("root");
if (container) { if (container) {
@ -11,6 +13,7 @@ if (container) {
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<Provider store={store}> <Provider store={store}>
<I18nextProvider i18n={i18n} />
<App /> <App />
</Provider> </Provider>
</React.StrictMode> </React.StrictMode>

View File

@ -1,18 +1,72 @@
import { useMsal } from "@azure/msal-react"; import { useMsal } from "@azure/msal-react";
import { loginRequest } from "common/msalConfig"; import { loginRequest } from "common/msalConfig";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next";
import { getTranslationID } from "translation";
import { LANGUAGE_LIST } from "../../features/top/constants";
const TopPage: React.FC = (): JSX.Element => { const TopPage: React.FC = (): JSX.Element => {
const { instance } = useMsal(); const { instance } = useMsal();
// eslint-disable-next-line
const [t, i18n] = useTranslation();
return ( return (
<div> <div>
<button <header>
type="button" <div>
onClick={() => instance.loginRedirect(loginRequest)} <img src="../../assets/images/OMS_logo_black.svg" alt="OM System" />
> </div>
sign in <p>ODMS Cloud</p>
</button> </header>
<main>
<section>
<div>
<dl>
<dt>{t(getTranslationID("topPage.label.displayLanguage"))}</dt>
<dd>
<select
onChange={(e) => {
i18n.changeLanguage(e.currentTarget.value);
// 既にcookieに選択言語があれば削除
document.cookie = "language=; max-age=0";
// cookieの期限は1年
document.cookie = `language=${e.currentTarget.value}; max-age=31536000`;
}}
>
{LANGUAGE_LIST.map((x) => (
<option key={x.value} value={x.value}>
{t(x.label)}
</option>
))}
</select>
</dd>
<dt>Already have an account?</dt>
<dd>
{/* eslint-disable */}
<a onClick={() => instance.loginRedirect(loginRequest)}>
Sign in
<img src="../../assets/images/arrow_forward.svg" alt="" />
</a>
</dd>
<dt>New user?</dt>
<dd>
<a href="●●">
Create a new account
<img
src="../../assets/images/arrow_forward_blue.svg"
alt=""
/>
</a>
</dd>
</dl>
</div>
</section>
</main>
<footer>
<div>&copy; OM Digital Solutions 2023</div>
</footer>
</div> </div>
); );
}; };

View File

@ -0,0 +1,11 @@
{
"topPage": {
"label": {
"displayLanguage": "(de)Display Language",
"languageEnglish": "(de)English",
"languageGerman": "(de)German",
"languageFrench": "(de)French",
"languageSpanish": "(de)Spanish"
}
}
}

View File

@ -0,0 +1,11 @@
{
"topPage": {
"label": {
"displayLanguage": "Display Language",
"languageEnglish": "English",
"languageGerman": "German",
"languageFrench": "French",
"languageSpanish": "Spanish"
}
}
}

View File

@ -0,0 +1,11 @@
{
"topPage": {
"label": {
"displayLanguage": "(es)Display Language",
"languageEnglish": "(es)English",
"languageGerman": "(es)German",
"languageFrench": "(es)French",
"languageSpanish": "(es)Spanish"
}
}
}

View File

@ -0,0 +1,11 @@
{
"topPage": {
"label": {
"displayLanguage": "(fr)Display Language",
"languageEnglish": "(fr)English",
"languageGerman": "(fr)German",
"languageFrench": "(fr)French",
"languageSpanish": "(fr)Spanish"
}
}
}

View File

@ -0,0 +1,17 @@
import en from "./en.json";
// 再帰的にプロパティ名リテラル型の結合を行う
// Pを列挙 => key-remapping(P)でPを元に(P.Pの子要素).(左の子要素)...という新しいプロパティ名を再帰的に作成する
type Key<T> = keyof {
[P in keyof T as T[P] extends string
? P
: Key<T[P]> extends string
? `${P extends string ? P : never}.${Key<T[P]>}`
: never]: T[P];
};
// IDの特定はen.jsonを基準とする
export type TranslationID = Key<typeof en>;
// ユーザーの入力補助用の関数
export const getTranslationID = (id: TranslationID) => id;