basic i18n support

This commit is contained in:
Xeltica 2021-09-14 09:22:04 +09:00
parent 7eeb90eddc
commit d06f2384dc
12 changed files with 115 additions and 19 deletions

View File

@ -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",

View File

@ -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<HeaderProps> = ({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<HeaderProps> = ({hasTopLink, children}) => {
<header className={'xarticle card shadow-4 mt-5 mb-3'}>
<div className="body">
<h1 className="text-primary mb-0" style={{ fontSize: '2rem' }}>
{hasTopLink ? <Link to="/"></Link> : 'みす廃アラート'}
{hasTopLink ? <Link to="/">{t('title')}</Link> : t('title')}
</h1>
<h2 className="text-dimmed ml-1">{message}</h2>
{children}

View File

@ -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<Pick<IUser,
| 'alertMode'
@ -53,7 +54,7 @@ export const SettingPage: React.VFC = () => {
];
const currentTheme = useSelector(state => state.screen.theme);
const [currentLang, setCurrentLang] = useState<string>('ja-JP');
const currentLang = useSelector(state => state.screen.lang);
const availableVisibilities: Visibility[] = [
'public',
@ -205,9 +206,14 @@ export const SettingPage: React.VFC = () => {
</div>
<h2></h2>
<select name="currentLang" className="input-field" value={currentLang} onChange={e => setCurrentLang(e.target.value)}>
<option value="ja-JP"></option>
<option value="en-US"></option>
<select name="currentLang" className="input-field" value={currentLang} onChange={(e) => {
dispatch(changeLang(e.target.value));
}}>
{
(Object.keys(languageName) as Array<keyof typeof languageName>).map(n => (
<option value={n} key={n}>{languageName[n]}</option>
))
}
</select>
</Card>

View File

@ -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/`;

View File

@ -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(<App/>, document.getElementById('app'));

5
src/frontend/json5.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare module '*.json5' {
const data: any;
export default data;
}

View File

@ -0,0 +1,5 @@
{
translation: {
title: "Misskey Alert",
}
}

View File

@ -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': '日本語',
};

View File

@ -0,0 +1,5 @@
{
translation: {
title: "みす廃アラート",
}
}

View File

@ -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<Theme>) => {
changeTheme: (state, action: PayloadAction<ScreenState['theme']>) => {
state.theme = action.payload;
localStorage[LOCALSTORAGE_KEY_THEME] = action.payload;
},
changeLang: (state, action: PayloadAction<ScreenState['language']>) => {
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;

View File

@ -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. */

View File

@ -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=