Skip to content

Commit

Permalink
Merge pull request #933 from kubosho/i18n
Browse files Browse the repository at this point in the history
Introduce i18n
  • Loading branch information
kubosho committed Jun 13, 2022
2 parents 9f59b44 + 6b767b0 commit f4fcb15
Show file tree
Hide file tree
Showing 19 changed files with 256 additions and 76 deletions.
3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -29,7 +29,8 @@
"option-t": "^32.2.1",
"prismjs": "^1.28.0",
"react": "^18.1.0",
"react-dom": "^18.1.0"
"react-dom": "^18.1.0",
"rosetta": "^1.1.0"
},
"devDependencies": {
"@mapbox/rehype-prism": "^0.8.0",
Expand Down
3 changes: 2 additions & 1 deletion src/components/EntryList.tsx
Expand Up @@ -7,6 +7,7 @@ import { formatYYMMDDString, formatISOString } from '../entry/date';
import { EntryValue } from '../entry/entry_value';

import styles from './EntryList.module.css';
import { retrieveTranslation } from '../locales/i18n';

interface Props {
title: string;
Expand Down Expand Up @@ -40,5 +41,5 @@ export const EntryList = ({ title, entries }: Props): JSX.Element =>
</ol>
</>
) : (
<p>記事はありません。</p>
<p>{retrieveTranslation('components.entryList.notFound')}</p>
);
8 changes: 5 additions & 3 deletions src/components/SiteMetadata.tsx
Expand Up @@ -3,19 +3,21 @@ import Head from 'next/head';
import { GTM_ID } from '../tracking/gtm_id';
import { createGAOptout } from '../tracking/ga_optout';
import { insertGtmScript } from '../tracking/gtm';
import { FAVICON_URL, OG_IMAGE_URL, SITE_TITLE, TWITTER_ACCOUNT_ID } from '../constants/site_data';
import { FAVICON_URL, OG_IMAGE_URL, TWITTER_ACCOUNT_ID } from '../constants/site_data';
import { PathList } from '../constants/path_list';
import { retrieveTranslation } from '../locales/i18n';

const BLUE_600 = '#003760';
const MAIN_COLOR = BLUE_600;

export const SiteMetadata = (): JSX.Element => {
const gaOptout = createGAOptout(GTM_ID);
const webSiteTitle = retrieveTranslation('website.title');

return (
<Head>
<meta name="theme-color" content={MAIN_COLOR} />
<meta property="og:site_name" content={SITE_TITLE} />
<meta property="og:site_name" content={webSiteTitle} />
<meta property="og:image" content={OG_IMAGE_URL} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
Expand All @@ -25,7 +27,7 @@ export const SiteMetadata = (): JSX.Element => {
<meta name="Hatena::Bookmark" content="nocomment" />
<link rel="apple-touch-icon" href={FAVICON_URL} />
<link rel="icon" type="image/png" href={FAVICON_URL} />
<link rel="alternate" type="application/atom+xml" href={PathList.Feed} title={SITE_TITLE} />
<link rel="alternate" type="application/atom+xml" href={PathList.Feed} title={webSiteTitle} />
{!gaOptout.enabled() && insertGtmScript(GTM_ID)}
</Head>
);
Expand Down
3 changes: 2 additions & 1 deletion src/components/icon/facebook.tsx
@@ -1,9 +1,10 @@
import React from 'react';
import { retrieveTranslation } from '../../locales/i18n';

// https://github.com/FortAwesome/Font-Awesome/blob/master/js-packages/@fortawesome/fontawesome-free/svgs/brands/facebook-f.svg
// License: https://fontawesome.com/license/free
export const FacebookSvg = (): JSX.Element => (
<svg viewBox="0 0 320 512" aria-label="Facebook でシェア" role="img">
<svg viewBox="0 0 320 512" aria-label={retrieveTranslation('components.icon.facebook')} role="img">
<path d="M279.14 288l14.22-92.66h-88.91v-60.13c0-25.35 12.42-50.06 52.24-50.06h40.42V6.26S260.43 0 225.36 0c-73.22 0-121.08 44.38-121.08 124.72v70.62H22.89V288h81.39v224h100.17V288z" />
</svg>
);
3 changes: 2 additions & 1 deletion src/components/icon/twitter.tsx
@@ -1,8 +1,9 @@
import React from 'react';
import { retrieveTranslation } from '../../locales/i18n';
// https://github.com/FortAwesome/Font-Awesome/blob/master/js-packages/@fortawesome/fontawesome-free/svgs/brands/twitter.svg
// License: https://fontawesome.com/license/free
export const TwitterSvg = (): JSX.Element => (
<svg viewBox="0 0 512 512" aria-label="Twitter でシェア" role="img">
<svg viewBox="0 0 512 512" aria-label={retrieveTranslation('components.icon.twitter')} role="img">
<path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z" />
</svg>
);
11 changes: 11 additions & 0 deletions src/locales/__tests__/i18n.test.ts
@@ -0,0 +1,11 @@
import { expect, test } from 'vitest';
import { activateI18n, retrieveTranslation, setLocale } from '../i18n';

test('i18n', () => {
activateI18n();
setLocale('ja');

const actual = retrieveTranslation('website.title');

expect(actual).toBe('学ぶ、考える、書き出す。');
});
47 changes: 47 additions & 0 deletions src/locales/i18n.ts
@@ -0,0 +1,47 @@
import rosetta, { Rosetta } from 'rosetta';

import { localeJa } from './ja';

export type SupportedLanguage = 'ja';

let i18n: Rosetta<unknown> | null = null;
let currentLanguage: SupportedLanguage | null = null;

export function activateI18n(): void {
if (i18n !== null) {
return;
}

i18n = rosetta({ ja: { ...localeJa } });
}

export function setLocale(lang: SupportedLanguage): void {
if (i18n === null) {
throw new Error('i18n is not activated');
}

if (currentLanguage === lang) {
return;
}

currentLanguage = lang;
i18n.locale(currentLanguage);
}

export function retrieveTranslation(
key: string | (string | number)[],
params?: unknown[] | Record<string, unknown>,
lang?: string,
): string {
if (i18n === null) {
throw new Error('i18n is not activated');
}

const r = i18n.t(key, params, lang);

if (r === '') {
throw new Error(`Translate text is not retrieved: ${key}`);
}

return r;
}
89 changes: 89 additions & 0 deletions src/locales/ja.ts
@@ -0,0 +1,89 @@
export const localeJa = {
website: {
title: '学ぶ、考える、書き出す。',
description: '学習し、自分なりに噛み砕いて、書き出すブログ。',
author: 'kubosho',
},
navigation: {
feed: 'フィード',
policy: 'ポリシー',
},
optout: {
actions: {
enabled: 'オプトアウトの有効化',
disabled: 'オプトアウトの無効化',
},
status: {
enabled: 'オプトアウトが有効になっています。Googleアナリティクスによるアクセス解析はおこなわれません。',
disabled: 'オプトアウトが無効になっています。Googleアナリティクスによるアクセス解析がおこなわれます。',
},
},
components: {
entryList: {
notFound: '記事はありません。',
},
icon: {
facebook: 'Facebookでシェア',
twitter: 'Twitterでシェア',
},
},
top: {
headings: {
entryList: '記事一覧',
},
},
draft: {
notAvailable: 'プレビューは利用できません。',
},
entry: {
share: '記事を共有する',
headings: {
related: '関連記事',
},
},
categories: {
description: '{{webSiteTitle}}の「{{category}}」に関連した記事の一覧です。',
headings: {
entryList: 'カテゴリー「{{category}}」の記事一覧',
},
},
tags: {
description: '{{webSiteTitle}}の「{{tag}}」に関連した記事の一覧です。',
headings: {
entryList: 'タグ「{{tag}}」の記事一覧',
},
},
policy: {
title: 'ポリシー',
headings: {
affiliate: 'アフィリエイト',
disclaimer: '免責事項',
optout: 'Googleアナリティクスによる解析のオプトアウト',
privacy: 'プライバシー',
},
intro: {
text: 'このページでは当ブログ『{{webSiteTitle}}』内で適用されるポリシーについて書きます。',
},
text: {
privacy: {
1: '当ブログでは内容の改善を目的として、Googleアナリティクスによるアクセス分析をおこなっています。',
2: 'Googleアナリティクスは、Cookie(クッキー)により、匿名のトラフィックデータを収集しています。',
3: 'Cookieに含まれるデータは利用者の個人情報を特定しません。利用者はCookieを無効にした状態で当ブログにアクセスできます。',
4: '詳しくはGoogleが公開している',
5: 'Google のサービスを使用するサイトやアプリから収集した情報の Google による使用',
6: 'のページを参照してください。',
},
optout: {
1: '以下のボタンからGoogleアナリティクスによる解析のオプトアウトの有効化・無効化がおこなえます。',
},
affiliate: {
1: '『{{webSiteTitle}}』は、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイトプログラムである、Amazonアソシエイト・プログラムの参加者です。',
},
disclaimer: {
1: '当ブログではコンテンツ・情報について、できる限り正確な情報を提供するように努めています。',
2: 'しかし、完全な正確性や安全性は保障いたしません。情報が古くなったり間違っていたりすることがあります。',
3: 'また、当ブログに掲載した内容によって生じた損害などの一切の責任は負いません。ご了承ください。',
},
},
},
};
17 changes: 10 additions & 7 deletions src/pages/_app.tsx
Expand Up @@ -5,7 +5,6 @@ import Link from 'next/link';
import { useRouter } from 'next/router';
import { isUndefined } from 'option-t/lib/Undefinable/Undefinable';

import { AUTHOR, SITE_DESCRIPTION, SITE_TITLE } from '../constants/site_data';
import { BUGSNAG_API_KEY } from '../constants/environment';
import { activateErrorBoundaryComponent } from '../components/ErrorBoundary';
import { PathList } from '../constants/path_list';
Expand All @@ -16,47 +15,51 @@ import '../common_styles/foundation.css';
import '../common_styles/site_specific.css';
import './variables.css';
import './app.page.css';
import { activateI18n, retrieveTranslation, setLocale } from '../locales/i18n';

function MyApp({ Component, pageProps }: AppProps): JSX.Element {
const router = useRouter();
const isDisplayedDescription = ![PathList.Entry, PathList.Feed, PathList.Policy].includes(
router.pathname as PathList,
);

activateI18n();
setLocale('ja');

const element = (
<>
<SiteMetadata />
<header className="site-header">
<h1 className="site-title">
<Link href={PathList.Root} passHref>
<a>{SITE_TITLE}</a>
<a>{retrieveTranslation('website.title')}</a>
</Link>
</h1>
{isDisplayedDescription && <p className="site-description">{SITE_DESCRIPTION}</p>}
{isDisplayedDescription && <p className="site-description">{retrieveTranslation('website.description')}</p>}
</header>
<Component {...pageProps} />
<footer className="site-footer">
<div className="site-links">
<p>
<Link href={PathList.Root} passHref>
<a>{SITE_TITLE}</a>
<a>{retrieveTranslation('website.title')}</a>
</Link>
</p>
<ul className="site-navigation">
<li>
<Link href={PathList.Feed}>
<a>フィード</a>
<a>{retrieveTranslation('navigation.feed')}</a>
</Link>
</li>
<li>
<Link href={PathList.Policy}>
<a>ポリシー</a>
<a>{retrieveTranslation('navigation.policy')}</a>
</Link>
</li>
</ul>
</div>
<p className="site-copyright">
<small>© {AUTHOR}</small>
<small>© {retrieveTranslation('website.author')}</small>
</p>
</footer>
</>
Expand Down
8 changes: 5 additions & 3 deletions src/pages/categories/[category].tsx
Expand Up @@ -6,8 +6,9 @@ import { EntryValue } from '../../entry/entry_value';
import { EntryList } from '../../components/EntryList';
import { SiteContents } from '../../components/SiteContents';
import { addSiteTitleToSuffix } from '../../site_title_inserter';
import { SITE_TITLE, SITE_URL } from '../../constants/site_data';
import { SITE_URL } from '../../constants/site_data';
import { getCategoryIdList, getEntryListByCategory } from '../../entry/entry_gateway';
import { retrieveTranslation } from '../../locales/i18n';

interface Props {
filteredEntries: Array<EntryValue>;
Expand All @@ -16,9 +17,10 @@ interface Props {

export const CategoryPage = (props: Props): JSX.Element => {
const { category, filteredEntries } = props;
const title = `${category}の記事一覧`;
const title = retrieveTranslation('categories.headings.entryList', { category });
const webSiteTitle = retrieveTranslation('website.title');
const titleInHead = addSiteTitleToSuffix(title);
const description = `${SITE_TITLE}の「${category}」に関連した記事の一覧です。`;
const description = retrieveTranslation('categories.description', { category, webSiteTitle });
const pageUrl = `${SITE_URL}/categories/${category}`;

const e = (
Expand Down
5 changes: 3 additions & 2 deletions src/pages/draft/[id].tsx
Expand Up @@ -17,6 +17,7 @@ import { PublishedDate } from '../../components/PublishedDate';
import { formatISOString, formatYYMMDDString } from '../../entry/date';
import { mapEntryValue } from '../../entry/entry_converter';
import { getEntryIdList } from '../../entry/entry_gateway';
import { retrieveTranslation } from '../../locales/i18n';

import styles from '../entry/entry.module.css';
import entryContentsChildrenStyles from '../entry/entryContentsChildren.module.css';
Expand All @@ -43,7 +44,7 @@ const Draft = (props: Props): JSX.Element => {
return (
<SiteContents>
<div className={entryContentsChildrenStyles['entry-contents']}>
<p>プレビューは利用できません。</p>
<p>{retrieveTranslation('draft.notAvailable')}</p>
</div>
</SiteContents>
);
Expand Down Expand Up @@ -94,7 +95,7 @@ const Draft = (props: Props): JSX.Element => {
</header>
<div className={entryContentsChildrenStyles['entry-contents']} dangerouslySetInnerHTML={{ __html: body }} />
<div className={styles['entry-share']}>
<p className={styles['entry-share-text']}>記事を共有する</p>
<p className={styles['entry-share-text']}>{retrieveTranslation('entry.share')}</p>
<SnsShare shareText={pageTitle} />
</div>
</article>
Expand Down
5 changes: 3 additions & 2 deletions src/pages/entry/[id].tsx
Expand Up @@ -14,6 +14,7 @@ import { addSiteTitleToSuffix } from '../../site_title_inserter';
import { getEntry, getEntrySlugList, getEntryListByCategory, getEntryListByTag } from '../../entry/entry_gateway';
import { createBlogPostingStructuredData } from '../../structured_data/blog_posting_structured_data';
import { getRelatedEntryList } from '../../entry/related_entry_list';
import { retrieveTranslation } from '../../locales/i18n';

import styles from './entry.module.css';
import entryContentsChildrenStyles from './entryContentsChildren.module.css';
Expand Down Expand Up @@ -88,13 +89,13 @@ const Entry = (props: Props): JSX.Element => {
</header>
<div className={entryContentsChildrenStyles['entry-contents']} dangerouslySetInnerHTML={{ __html: body }} />
<div className={styles['entry-share']}>
<p className={styles['entry-share-text']}>記事を共有する</p>
<p className={styles['entry-share-text']}>{retrieveTranslation('entry.share')}</p>
<SnsShare shareText={pageTitle} />
</div>
</article>
{relatedEntryList.length > 0 && (
<section className={styles['related-entry-list']}>
<h2>関連記事</h2>
<h2>{retrieveTranslation('entry.headings.related')}</h2>
<ul>
{relatedEntryList.map(({ id, title }) => (
<li key={id}>
Expand Down

1 comment on commit f4fcb15

@vercel
Copy link

@vercel vercel bot commented on f4fcb15 Jun 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

im-kubosho – ./

im-kubosho-git-master-kubosho.vercel.app
blog.kubosho.com
im-kubosho-kubosho.vercel.app

Please sign in to comment.