Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d7e22b6
feat: implement i18n support with Japanese and English languages
claude[bot] Sep 1, 2025
a2bc8d5
fix: deps version
nukopy Sep 2, 2025
5a2cfc6
chore: format
nukopy Sep 2, 2025
7db7ab2
fix: lint error
nukopy Sep 2, 2025
19f42d9
fix: import error
nukopy Sep 2, 2025
fe7595b
fix: import error
nukopy Sep 2, 2025
98b5453
refactor: use dynamic routing for i18n instead of duplicate route files
claude[bot] Sep 2, 2025
501146e
feat: put language switcher on right-side of theme switcher
nukopy Sep 2, 2025
78283c4
chore: format
nukopy Sep 2, 2025
4e48472
chore: modify npm scripts name
nukopy Sep 2, 2025
6bd36d8
fix: type error
nukopy Sep 2, 2025
a41baaa
docs: japanize content
nukopy Sep 2, 2025
d220a02
fix: type error
nukopy Sep 2, 2025
3f60db2
chore: reinstall i18n dependencies
nukopy Sep 2, 2025
ad76461
feat: fix layout
nukopy Sep 2, 2025
338b2ae
feat: impl middleware error, logging
nukopy Sep 2, 2025
75ea8a2
feat: add util routes
nukopy Sep 2, 2025
c5c38c3
refactor: for multi language
nukopy Sep 2, 2025
11bb74f
fix: impl i18n basis
nukopy Sep 2, 2025
bd97a3b
feat: impl contexts for middleware
nukopy Sep 2, 2025
52db446
feat: enable React Router v7 middleware
nukopy Sep 2, 2025
692e7ac
fix: remove unnecessary component
nukopy Sep 2, 2025
28d3bc1
feat: modify error, logging middlewares
nukopy Sep 4, 2025
c050f65
feat: impl "About" page
nukopy Sep 4, 2025
091ea83
chore: add lucide-react for icons
nukopy Sep 4, 2025
891105c
feat&refactor: make page transition smoothly (change a tag to Link co…
nukopy Sep 4, 2025
962e3ac
feat: add misc page and some loggings for understanding React Router v7
nukopy Sep 4, 2025
f2346f9
perf: display image as static image assets, not as React component fo…
nukopy Sep 4, 2025
71427a2
style: fix layout
nukopy Sep 4, 2025
cbb469c
refactor: make cookie key name easier to understand
nukopy Sep 4, 2025
73694ff
fix: lint error
nukopy Sep 4, 2025
2b04440
feat: enhance logging for debug
nukopy Sep 4, 2025
b1a3f1b
feat: impl prefetch for Links
nukopy Sep 4, 2025
cb0d4e3
chore: add comments to understand React Router v7's process
nukopy Sep 4, 2025
6fc06ec
tmp
nukopy Aug 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions app/components/LanguageSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useTranslation } from "react-i18next";
import { SUPPORTED_LANGUAGES } from "../lib/i18n";

export function LanguageSwitcher() {
const { t, i18n } = useTranslation();

const handleLanguageChange = (lng: string) => {
// Update URL to include/remove language prefix
const currentPath = window.location.pathname;
let newPath: string;

if (lng === "ja") {
// Remove language prefix for Japanese (default)
if (currentPath.startsWith("/en")) {
newPath = currentPath.replace("/en", "") || "/";
} else {
newPath = currentPath;
}
} else {
// Add language prefix for other languages
if (currentPath.startsWith("/en")) {
newPath = currentPath;
} else if (currentPath === "/") {
newPath = `/${lng}`;
} else {
newPath = `/${lng}${currentPath}`;
}
}
console.info(
`[LanguageSwitcher] changing language to ${lng}, newPath: ${newPath}`,
);

window.history.pushState({}, "", newPath);

// FIXME: cookie 更新されてる?
// FIXME: ここで更新後、次回 http://localhost:5173/ にアクセスしたとき、自動で /en にリダイレクトさせたい

// redirect to new path
window.location.href = newPath;
};

return (
<div className="dropdown dropdown-end">
<button
type="button"
className="btn btn-ghost btn-circle"
title={t("language.title")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
role="img"
aria-label={t("language.title")}
>
<path
fill="currentColor"
d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"
/>
</svg>
</button>
<ul className="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow-2xl">
<li className="menu-title">
<span>{t("language.select")}</span>
</li>
{SUPPORTED_LANGUAGES.map((lng) => (
<li key={lng}>
<button
type="button"
className={`flex items-center gap-2 ${
i18n.language === lng ? "active" : ""
}`}
onClick={() => {
handleLanguageChange(lng);
// ドロップダウンを閉じるためにフォーカスを外す
(document.activeElement as HTMLElement)?.blur();
}}
>
<span>{t(`language.${lng}`)}</span>
{i18n.language === lng && (
<svg
className="w-4 h-4 ml-auto"
fill="currentColor"
viewBox="0 0 20 20"
role="img"
aria-label={t("language.selected")}
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
)}
</button>
</li>
))}
</ul>
</div>
);
}
6 changes: 5 additions & 1 deletion app/components/ThemeSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export function ThemeSwitcher() {
className={`flex items-center gap-2 ${
currentTheme === theme.name ? "active" : ""
}`}
onClick={() => handleThemeChange(theme.name)}
onClick={() => {
handleThemeChange(theme.name);
// ドロップダウンを閉じるためにフォーカスを外す
(document.activeElement as HTMLElement)?.blur();
}}
>
<span>{theme.label}</span>
{currentTheme === theme.name && (
Expand Down
21 changes: 21 additions & 0 deletions app/components/icons/GitHubIconSvg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* GitHub svg icon
* from simpleicons.org: https://simpleicons.org/?q=github
*/
export default function GitHubIconSvg({
className = "w-5 h-5",
}: {
className?: string;
}) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
);
}
11 changes: 11 additions & 0 deletions app/components/icons/LinkedInIconPng.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* LinkedIn png icon
*/
export default function LinkedInIconPng({
className = "w-5 h-5",
}: {
className?: string;
}) {
const src = "/images/LinkedIn/LinkedInIconBackgroundBlack.png";
return <img src={src} alt="LinkedIn Icon" className={className} />;
}
11 changes: 11 additions & 0 deletions app/components/icons/NukopyIconPng.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Nukopy png icon
*/
export default function NukopyIconPng({
className = "w-5 h-5",
}: {
className?: string;
}) {
const src = "/images/NukopyIcon.png";
return <img src={src} alt="Nucopy Icon" className={className} />;
}
21 changes: 21 additions & 0 deletions app/components/icons/XIconSvg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* X svg icon
* from simpleicons.org: https://simpleicons.org/?q=x
*/
export default function GitHubIconSvg({
className = "w-5 h-5",
}: {
className?: string;
}) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<title>X</title>
<path d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" />
</svg>
);
}
21 changes: 21 additions & 0 deletions app/components/icons/ZennIconSvg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Zenn svg icon
* from simpleicons.org: https://simpleicons.org/?q=zenn
*/
export default function ZennIconSvg({
className = "w-5 h-5",
}: {
className?: string;
}) {
return (
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<title>Zenn</title>
<path d="M.264 23.771h4.984c.264 0 .498-.147.645-.352L19.614.874c.176-.293-.029-.645-.381-.645h-4.72c-.235 0-.44.117-.557.323L.03 23.361c-.088.176.029.41.234.41zM17.445 23.419l6.479-10.408c.205-.323-.029-.733-.41-.733h-4.691c-.176 0-.352.088-.44.235l-6.655 10.643c-.176.264.029.616.352.616h4.779c.234-.001.468-.118.586-.353z" />
</svg>
);
}
31 changes: 31 additions & 0 deletions app/components/icons/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { BookOpenIcon, User } from "lucide-react";
import type { JSX } from "react";
import GitHubIconSvg from "./GitHubIconSvg";
import LinkedInIconPng from "./LinkedInIconPng";
import NukopyIconPng from "./NukopyIconPng";
import XIconSvg from "./XIconSvg";
import ZennIconSvg from "./ZennIconSvg";

export type IconType = "github" | "x" | "zenn" | "linkedin" | "blog" | "nukopy";

export function getIconByType(
iconType: IconType,
className?: string,
): JSX.Element {
switch (iconType) {
case "github":
return <GitHubIconSvg className={className} />;
case "x":
return <XIconSvg className={className} />;
case "zenn":
return <ZennIconSvg className={className} />;
case "linkedin":
return <LinkedInIconPng className={className} />;
case "blog":
return <BookOpenIcon className={className} />;
case "nukopy":
return <NukopyIconPng className={className} />;
default:
return <User className={className} />;
}
}
75 changes: 57 additions & 18 deletions app/components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
import { Link } from "react-router";
import { LanguageSwitcher } from "../LanguageSwitcher";
import { ThemeSwitcher } from "../ThemeSwitcher";

const MENU_ITEMS = [
{
id: "contents",
href: "/contents",
label: "Contents",
},
{
id: "tags",
href: "/tags",
label: "Tags",
},
{
id: "about",
href: "/about",
label: "About",
},
{
id: "misc",
href: "/misc",
label: "Misc",
},
];

export function Header() {
return (
<header className="navbar bg-base-200 border-b border-base-300 sticky top-0 z-50">
<div className="navbar-start">
<div className="dropdown">
<button type="button" className="btn btn-ghost lg:hidden">
{/* dropdown menu displayed when small screen, hidden on lg */}
<div className="dropdown dropdown-bottom">
<button type="button" tabIndex={0} className="btn lg:hidden">
<svg
className="w-5 h-5"
fill="none"
Expand All @@ -22,31 +48,44 @@ export function Header() {
/>
</svg>
</button>
<ul className="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow">
<li>
<a href="/contents">Contents</a>
</li>
<li>
<a href="/tags">Tags</a>
</li>
<ul
// biome-ignore lint/a11y/noNoninteractiveTabindex: to close when focus is lost
tabIndex={0}
className="menu dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-30 p-2 shadow"
>
{MENU_ITEMS.map((item) => (
<li key={item.id}>
<Link
to={item.href}
onClick={() => {
// ドロップダウンを閉じるためにフォーカスを外す
(document.activeElement as HTMLElement)?.blur();
}}
>
{item.label}
</Link>
</li>
))}
</ul>
</div>
<a href="/" className="btn btn-ghost text-xl">

{/* logo */}
<Link to="/" className="btn btn-ghost text-xl">
Chasing the Kernel
</a>
</Link>
</div>
<div className="navbar-center hidden lg:flex">
<div className="navbar-end hidden lg:flex">
<ul className="menu menu-horizontal px-1">
<li>
<a href="/contents">Contents</a>
</li>
<li>
<a href="/tags">Tags</a>
</li>
{MENU_ITEMS.map((item) => (
<li key={item.id}>
<Link to={item.href}>{item.label}</Link>
</li>
))}
</ul>
</div>
<div className="navbar-end">
<ThemeSwitcher />
<LanguageSwitcher />
</div>
</header>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ summary: "This is my first post!"
# Hello world

This is my first post!
... rest of the content
hoge
... rest of the content in English
hoge
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ summary: "This is my first document!"

# My first document

howhow...
This is my first document in English...
2 changes: 1 addition & 1 deletion app/contents/test-mdx.mdx → app/contents/en/test-mdx.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ import Counter from "./components/Counter.tsx";

# Test MDX

<Counter />
<Counter />
11 changes: 11 additions & 0 deletions app/contents/ja/hello-world.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: "こんにちは、世界"
tags: ["hello", "world"]
summary: "これははじめてのポストです。"
---

# こんにちは、世界

これははじめてのポストです。

ほげ
9 changes: 9 additions & 0 deletions app/contents/ja/my-first-document.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: "はじめてのドキュメント"
tags: ["operating system", "unix"]
summary: "これははじめてのドキュメントです。"
---

# はじめてのドキュメント

ほうほう
11 changes: 11 additions & 0 deletions app/contents/ja/test-mdx.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: "MDX のテスト"
tags: ["react", "mdx"]
summary: "MDX のテストです。"
---

import Counter from "./components/Counter.tsx";

# Test MDX

<Counter />
Loading
Loading