From d06f2384dcb65a07a7723b8cf2fdc29147c9b58c Mon Sep 17 00:00:00 2001 From: Xeltica Date: Tue, 14 Sep 2021 09:22:04 +0900 Subject: [PATCH] basic i18n support --- package.json | 3 +++ src/frontend/components/Header.tsx | 6 ++++- src/frontend/components/SettingPage.tsx | 22 +++++++++++------ src/frontend/const.ts | 3 ++- src/frontend/init.tsx | 17 +++++++++++++ src/frontend/json5.d.ts | 5 ++++ src/frontend/langs/en_US.json5 | 5 ++++ src/frontend/langs/index.ts | 12 +++++++++ src/frontend/langs/ja_JP.json5 | 5 ++++ src/frontend/store/slices/screen.ts | 15 ++++++++--- tsconfig.json | 8 +++--- yarn.lock | 33 +++++++++++++++++++++++-- 12 files changed, 115 insertions(+), 19 deletions(-) create mode 100644 src/frontend/json5.d.ts create mode 100644 src/frontend/langs/en_US.json5 create mode 100644 src/frontend/langs/index.ts create mode 100644 src/frontend/langs/ja_JP.json5 diff --git a/package.json b/package.json index f90eba8..dd023c9 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,8 @@ "dayjs": "^1.10.2", "delay": "^4.4.0", "fibers": "^5.0.0", + "i18next": "^20.6.1", + "i18next-browser-languagedetector": "^6.1.2", "json5-loader": "^4.0.1", "koa": "^2.13.0", "koa-bodyparser": "^4.3.0", @@ -59,6 +61,7 @@ "pug": "^3.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-i18next": "^11.12.0", "react-modal-hook": "^3.0.0", "react-redux": "^7.2.4", "react-router-dom": "^5.2.1", diff --git a/src/frontend/components/Header.tsx b/src/frontend/components/Header.tsx index e603746..8bdbef6 100644 --- a/src/frontend/components/Header.tsx +++ b/src/frontend/components/Header.tsx @@ -2,11 +2,15 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { welcomeMessage } from '../misc/welcome-message'; +import { useTranslation } from 'react-i18next'; + export type HeaderProps = { hasTopLink?: boolean; }; export const Header: React.FC = ({hasTopLink, children}) => { + const { t } = useTranslation(); + const message = React.useMemo( () => welcomeMessage[Math.floor(Math.random() * welcomeMessage.length)] , []); @@ -14,7 +18,7 @@ export const Header: React.FC = ({hasTopLink, children}) => {

- {hasTopLink ? みす廃アラート : 'みす廃アラート'} + {hasTopLink ? {t('title')} : t('title')}

{message}

{children} diff --git a/src/frontend/components/SettingPage.tsx b/src/frontend/components/SettingPage.tsx index 48c231c..82e12c8 100644 --- a/src/frontend/components/SettingPage.tsx +++ b/src/frontend/components/SettingPage.tsx @@ -1,15 +1,16 @@ -import React, { useCallback, useEffect, useReducer, useState } from 'react'; +import React, { useCallback, useEffect, useReducer } from 'react'; import { alertModes } from '../../common/types/alert-mode'; import { IUser } from '../../common/types/user'; -import { visibilities, Visibility } from '../../common/types/visibility'; +import { Visibility } from '../../common/types/visibility'; import { useGetSessionQuery } from '../services/session'; import { defaultTemplate } from '../../common/default-template'; import { Card } from './Card'; import { Theme } from '../misc/theme'; -import { API_ENDPOINT, LOCALSTORAGE_KEY_TOKEN } from '../const'; +import { API_ENDPOINT, LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_TOKEN } from '../const'; import { useDispatch } from 'react-redux'; -import { changeTheme, showModal } from '../store/slices/screen'; +import { changeLang, changeTheme, showModal } from '../store/slices/screen'; import { useSelector } from '../store'; +import { languageName } from '../langs'; type SettingDraftType = Partial { ]; const currentTheme = useSelector(state => state.screen.theme); - const [currentLang, setCurrentLang] = useState('ja-JP'); + const currentLang = useSelector(state => state.screen.lang); const availableVisibilities: Visibility[] = [ 'public', @@ -205,9 +206,14 @@ export const SettingPage: React.VFC = () => {

言語設定

- { + dispatch(changeLang(e.target.value)); + }}> + { + (Object.keys(languageName) as Array).map(n => ( + + )) + } diff --git a/src/frontend/const.ts b/src/frontend/const.ts index e530223..4ba3846 100644 --- a/src/frontend/const.ts +++ b/src/frontend/const.ts @@ -1,4 +1,5 @@ export const LOCALSTORAGE_KEY_TOKEN = 'token'; -export const LOCALSTORAGE_KEY_THEME = 'THEME'; +export const LOCALSTORAGE_KEY_THEME = 'theme'; +export const LOCALSTORAGE_KEY_LANG = 'lang'; export const API_ENDPOINT = `//${location.host}/api/v1/`; diff --git a/src/frontend/init.tsx b/src/frontend/init.tsx index 2e2aff9..a1f3a80 100644 --- a/src/frontend/init.tsx +++ b/src/frontend/init.tsx @@ -1,7 +1,11 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + import { App } from './App'; import { LOCALSTORAGE_KEY_TOKEN } from './const'; +import { resources } from './langs'; document.body.classList.add('dark'); @@ -18,4 +22,17 @@ if (token) { document.cookie = ''; } +console.log(resources); + +i18n + .use(initReactI18next) + .init({ + resources, + lng: localStorage['lang'] ?? 'ja_JP', + interpolation: { + escapeValue: false // react already safes from xss + } + }); + + ReactDOM.render(, document.getElementById('app')); diff --git a/src/frontend/json5.d.ts b/src/frontend/json5.d.ts new file mode 100644 index 0000000..8590b33 --- /dev/null +++ b/src/frontend/json5.d.ts @@ -0,0 +1,5 @@ +declare module '*.json5' { + const data: any; + + export default data; +} diff --git a/src/frontend/langs/en_US.json5 b/src/frontend/langs/en_US.json5 new file mode 100644 index 0000000..a2f2e62 --- /dev/null +++ b/src/frontend/langs/en_US.json5 @@ -0,0 +1,5 @@ +{ + translation: { + title: "Misskey Alert", + } +} diff --git a/src/frontend/langs/index.ts b/src/frontend/langs/index.ts new file mode 100644 index 0000000..9ef25a0 --- /dev/null +++ b/src/frontend/langs/index.ts @@ -0,0 +1,12 @@ +import enUS from './en_US.json5'; +import jaJP from './ja_JP.json5'; + +export const resources = { + 'en_US': enUS, + 'ja_JP': jaJP, +}; + +export const languageName = { + 'en_US': 'English', + 'ja_JP': '日本語', +}; diff --git a/src/frontend/langs/ja_JP.json5 b/src/frontend/langs/ja_JP.json5 new file mode 100644 index 0000000..d69ddca --- /dev/null +++ b/src/frontend/langs/ja_JP.json5 @@ -0,0 +1,5 @@ +{ + translation: { + title: "みす廃アラート", + } +} diff --git a/src/frontend/store/slices/screen.ts b/src/frontend/store/slices/screen.ts index e9a776e..ec37d69 100644 --- a/src/frontend/store/slices/screen.ts +++ b/src/frontend/store/slices/screen.ts @@ -1,5 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { LOCALSTORAGE_KEY_THEME } from '../../const'; +import i18n from 'i18next'; + +import { LOCALSTORAGE_KEY_LANG, LOCALSTORAGE_KEY_THEME } from '../../const'; import { Theme } from '../../misc/theme'; import { Modal } from '../../modal/modal'; @@ -7,12 +9,14 @@ interface ScreenState { modal: Modal | null; modalShown: boolean; theme: Theme; + language: string; } const initialState: ScreenState = { modal: null, modalShown: false, theme: localStorage[LOCALSTORAGE_KEY_THEME] ?? 'system', + language: localStorage[LOCALSTORAGE_KEY_LANG] ?? i18n.language ?? 'ja_JP', }; export const screenSlice = createSlice({ @@ -27,13 +31,18 @@ export const screenSlice = createSlice({ state.modal = null; state.modalShown = false; }, - changeTheme: (state, action: PayloadAction) => { + changeTheme: (state, action: PayloadAction) => { state.theme = action.payload; localStorage[LOCALSTORAGE_KEY_THEME] = action.payload; }, + changeLang: (state, action: PayloadAction) => { + state.language = action.payload; + localStorage[LOCALSTORAGE_KEY_LANG] = action.payload; + i18n.changeLanguage(action.payload); + }, }, }); -export const { showModal, hideModal, changeTheme } = screenSlice.actions; +export const { showModal, hideModal, changeTheme, changeLang } = screenSlice.actions; export default screenSlice.reducer; diff --git a/tsconfig.json b/tsconfig.json index ae48cc8..01e2a16 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,10 +6,10 @@ "rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "strict": true, /* Enable all strict type-checking options. */ "strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */ - "typeRoots": [ - "node_modules/@types", - "src/@types" - ], /* List of folders to include type definitions from. */ + "typeRoots": [ + "node_modules/@types", + "src/@types" + ], /* List of folders to include type definitions from. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ diff --git a/yarn.lock b/yarn.lock index 662b491..09944e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -150,7 +150,7 @@ "@babel/plugin-transform-react-jsx-development" "^7.14.5" "@babel/plugin-transform-react-pure-annotations" "^7.14.5" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.14.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.9.2": version "7.15.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== @@ -2525,6 +2525,13 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + http-assert@^1.3.0: version "1.5.0" resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.5.0.tgz#c389ccd87ac16ed2dfa6246fd73b926aa00e6b8f" @@ -2586,6 +2593,20 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +i18next-browser-languagedetector@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.2.tgz#68565a28b929cbc98ab6a56826ef2faf0e927ff8" + integrity sha512-YDzIGHhMRvr7M+c8B3EQUKyiMBhfqox4o1qkFvt4QXuu5V2cxf74+NCr+VEkUuU0y+RwcupA238eeolW1Yn80g== + dependencies: + "@babel/runtime" "^7.14.6" + +i18next@^20.6.1: + version "20.6.1" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.6.1.tgz#535e5f6e5baeb685c7d25df70db63bf3cc0aa345" + integrity sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A== + dependencies: + "@babel/runtime" "^7.12.0" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -4270,6 +4291,14 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-i18next@^11.12.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.12.0.tgz#2a053321b9b7a876d5baa7af55a12d986117bffc" + integrity sha512-M9BT+hqVG03ywrl+L7CK74ugK+4jIo7AeKJ17+g9BoqJz2+/aVbs8SIVXT4KMQ1rjIdcw+GcSRDy1CXjcz6tLQ== + dependencies: + "@babel/runtime" "^7.14.5" + html-parse-stringify "^3.0.1" + react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -5420,7 +5449,7 @@ vary@^1.1.2, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -void-elements@^3.1.0: +void-elements@3.1.0, void-elements@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=