このコミットが含まれているのは:
2026-10-17 23:01:24 +09:00
コミット 91296f7477
21個のファイルの変更、664行の追加、113行の削除
+53
ファイルの表示
@@ -0,0 +1,53 @@
import { Children, cloneElement, Fragment, isValidElement } from 'react'
import { cn } from '@/lib/utils'
import type { ReactNode } from 'react'
export const formatUIString = (text: string, language: string): string =>
(language === 'ain' || language === 'ain-Kana')
? text.replace (/[ \t\r\n]+/g, '\u3000')
: text
export const localiseUIChildren = (children: ReactNode, language: string): ReactNode => {
if (language !== 'ain' && language !== 'ain-Kana')
return children
return Children.map (children, child => {
if (typeof child === 'string')
{
return formatUIString (child, language).split (/(\u3000+)/).map ((part, index) =>
part.includes ('\u3000')
? <Fragment key={index}>{part}<wbr/></Fragment>
: <span key={index} className="ainu-ui-word">{part}</span>)
}
if (isValidElement<{ children?: ReactNode; lang?: string }> (child))
{
if (child.type === 'code' || child.type === 'pre'
|| (child.props.lang != null && !(child.props.lang.startsWith ('ain'))))
return child
return cloneElement (child, { }, localiseUIChildren (child.props.children, language))
}
return child
})
}
const LocalisedText = (
{ children, language, className }: {
children: ReactNode
language: string
className?: string },
) => (
<span lang={language === 'ain' ? 'ain-Kana' : language}
className={cn ((language === 'ain' || language === 'ain-Kana') && 'ainu-ui-text',
className)}>
{localiseUIChildren (children, language)}
</span>)
export default LocalisedText
+89 -59
ファイルの表示
@@ -5,9 +5,11 @@ import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import LocalisedText, { formatUIString } from '@/components/LocalisedText'
import Separator from '@/components/MenuSeparator'
import PrefetchLink from '@/components/PrefetchLink'
import TopNavUser from '@/components/TopNavUser'
import TranslatedText from '@/components/TranslatedText'
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
import { isLocaleCode,
localeFromPath,
@@ -41,7 +43,13 @@ export const menuOutline = (
): Menu => {
const pathName = unlocalisedPath (fullPathname)
const locale = localeFromPath (fullPathname) ?? 'ja'
const message = (key: Parameters<typeof uiMessage>[1]) => uiMessage (locale, key)
const message = (
key: Parameters<typeof uiMessage>[1],
values?: Record<string, string | number>,
) => ({ name: uiMessage (locale, key, values),
nameLanguage: uiMessageLanguage (locale, key) })
const postCount = tag?.postCount ?? material?.tag?.postCount ?? 0
const editable = canEditContent (user)
@@ -56,63 +64,63 @@ export const menuOutline = (
const helpIdentifier = pathName.split ('/')[2] ?? ''
return [
{ name: message ('square'), to: '/posts', subMenu: [
{ name: message ('list'), to: '/posts' },
{ name: message ('search'), to: '/posts/search' },
{ name: message ('add'), to: '/posts/new', visible: editable },
{ name: message ('allHistory'), to: '/posts/changes' },
{ name: message ('help'), to: '/wiki/ヘルプ:広場' }] },
{ name: message ('tags'), to: '/tags', subMenu: [
{ name: message ('master'), to: '/tags' },
{ name: message ('nicoLinks'), to: '/tags/nico' },
{ name: message ('allHistory'), to: '/tags/changes' },
{ name: message ('help'), to: '/wiki/ヘルプ:タグ' },
{ ...message ('posts'), to: '/posts', subMenu: [
{ ...message ('list'), to: '/posts' },
{ ...message ('search'), to: '/posts/search' },
{ ...message ('add'), to: '/posts/new', visible: editable },
{ ...message ('allHistory'), to: '/posts/changes' },
{ ...message ('help'), to: '/wiki/ヘルプ:広場' }] },
{ ...message ('tags'), to: '/tags', subMenu: [
{ ...message ('master'), to: '/tags' },
{ ...message ('nicoLinks'), to: '/tags/nico' },
{ ...message ('allHistory'), to: '/tags/changes' },
{ ...message ('help'), to: '/wiki/ヘルプ:タグ' },
{ component: <Separator/>, visible: tagFlg },
{ name: `広場 (${ postCount || 0 })`,
{ ...message ('postsWithCount', { count: postCount }),
to: `/posts?tags=${ encodeURIComponent (tag?.name ?? '') }`,
visible: tagFlg },
{ name: message ('history'), to: `/tags/changes?id=${ tag?.id }`,
{ ...message ('history'), to: `/tags/changes?id=${ tag?.id }`,
visible: tagFlg && tag?.category !== 'nico' }] },
{ name: message ('materials'), to: '/materials', visible: true, subMenu: [
{ name: message ('list'), to: '/materials' },
{ name: message ('add'), to: '/materials/new', visible: editable },
{ name: message ('suppressions'), to: '/materials/suppressions',
{ ...message ('materials'), to: '/materials', visible: true, subMenu: [
{ ...message ('list'), to: '/materials' },
{ ...message ('add'), to: '/materials/new', visible: editable },
{ ...message ('suppressions'), to: '/materials/suppressions',
visible: user?.role === 'admin' },
{ name: message ('allHistory'), to: '/materials/changes' },
{ name: message ('help'), to: '/wiki/ヘルプ:素材管理' },
{ ...message ('allHistory'), to: '/materials/changes' },
{ ...message ('help'), to: '/wiki/ヘルプ:素材管理' },
{ component: <Separator/>, visible: materialFlg },
{ name: `広場 (${ postCount || 0 })`,
{ ...message ('postsWithCount', { count: postCount }),
to: `/posts?tags=${ encodeURIComponent (material?.tag?.name ?? '') }`,
visible: materialFlg && Boolean (material?.tag) },
{ name: message ('history'), to: `/materials/changes?material_id=${ material?.id }`,
{ ...message ('history'), to: `/materials/changes?material_id=${ material?.id }`,
visible: materialFlg }] },
{ name: message ('wiki'), to: '/wiki', subMenu: [
{ name: message ('search'), to: '/wiki' },
{ name: message ('create'), to: '/wiki/new', visible: editable },
{ name: message ('allHistory'), to: '/wiki/changes' },
{ name: message ('help'), to: '/wiki/ヘルプ:Wiki' },
{ ...message ('wiki'), to: '/wiki', subMenu: [
{ ...message ('search'), to: '/wiki' },
{ ...message ('create'), to: '/wiki/new', visible: editable },
{ ...message ('allHistory'), to: '/wiki/changes' },
{ ...message ('help'), to: '/wiki/ヘルプ:Wiki' },
{ component: <Separator/>, visible: wikiPageFlg },
{ name: `広場 (${ postCount || 0 })`, to: `/posts?tags=${ wikiTitle }`,
{ ...message ('postsWithCount', { count: postCount }), to: `/posts?tags=${ wikiTitle }`,
visible: wikiPageFlg },
{ name: message ('history'), to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
{ name: message ('edit'), to: `/wiki/${ wikiId || wikiTitle }/edit`,
{ ...message ('history'), to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
{ ...message ('edit'), to: `/wiki/${ wikiId || wikiTitle }/edit`,
visible: wikiPageFlg && editable }] },
{ name: message ('help'), to: '/help/home', base: '/help', subMenu: [
{ name: message ('search'), to: '/help' },
{ name: message ('allHistory'), to: '/help/changes' },
{ ...message ('help'), to: '/help/home', base: '/help', subMenu: [
{ ...message ('search'), to: '/help' },
{ ...message ('allHistory'), to: '/help/changes' },
{ component: <Separator/>, visible: helpVsbl },
{ name: message ('history'),
{ ...message ('history'),
to: `/help/changes?identifier=${ helpIdentifier }`,
visible: helpVsbl }] },
{ name: message ('entertainment'), visible: false, subMenu: [
{ name: message ('theatre'), to: '/theatres/1' },
{ name: message ('gekanator'), to: '/gekanator' }] },
{ name: message ('users'), to: '/users/settings', visible: false, subMenu: [
{ name: message ('list'), to: '/users', visible: false },
{ name: message ('currentUser'), to: `/users/${ user?.id }`, visible: false },
{ name: message ('settings'), to: '/users/settings', visible: Boolean (user) }] },
{ name: message ('legal'), visible: false, subMenu: [
{ name: message ('terms'), to: '/tos' }] }]
{ ...message ('entertainment'), visible: false, subMenu: [
{ ...message ('theatre'), to: '/theatres/1' },
{ ...message ('gekanator'), to: '/gekanator' }] },
{ ...message ('users'), to: '/users/settings', visible: false, subMenu: [
{ ...message ('list'), to: '/users', visible: false },
{ ...message ('currentUser'), to: `/users/${ user?.id }`, visible: false },
{ ...message ('settings'), to: '/users/settings', visible: Boolean (user) }] },
{ ...message ('legal'), visible: false, subMenu: [
{ ...message ('terms'), to: '/tos' }] }]
}
@@ -315,10 +323,10 @@ const TopNav: FC<Props> = ({ user, locales }) => {
rounded px-1 focus-within:ring-2">
<Globe aria-hidden="true" size={16}/>
<span className="top-nav-locale-name whitespace-nowrap text-sm">
{localeName}
<LocalisedText language={locale}>{localeName}</LocalisedText>
</span>
<select
aria-label={localeName}
aria-label={formatUIString (localeName ?? '', locale)}
value={locale}
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
onChange={event => {
@@ -331,7 +339,7 @@ const TopNav: FC<Props> = ({ user, locales }) => {
}}>
{locales.map (entry => isLocaleCode (entry.code) && (
<option key={entry.code} value={entry.code}>
{entry.name}
{formatUIString (entry.name, entry.code)}
</option>))}
</select>
</label>)
@@ -339,7 +347,7 @@ const TopNav: FC<Props> = ({ user, locales }) => {
return (
<div ref={layoutRef} className="top-nav-layout w-full min-w-0 shrink-0"
data-desktop={desktop} data-density={density}>
<nav ref={barRef} lang={uiMessageLanguage (locale, 'square')}
<nav ref={barRef} lang={uiMessageLanguage (locale, 'posts')}
className="top-nav-root top-nav-bar flex justify-between items-center w-full">
<div className="top-nav-primary flex shrink-0 items-center h-12">
<PrefetchLink
@@ -348,6 +356,7 @@ const TopNav: FC<Props> = ({ user, locales }) => {
onClick={() => {
scroll (0, 0)
}}>
{/* TODO: ここも外出ししろ. */}
ぼざクリ タグ広場
</PrefetchLink>
@@ -380,7 +389,9 @@ const TopNav: FC<Props> = ({ user, locales }) => {
}}
className={cn ('top-nav-menu-link relative z-10 flex h-full items-center',
(i === openItemIdx) && 'font-bold')}>
{item.name}
<LocalisedText language={item.nameLanguage ?? 'ja'}>
{item.name}
</LocalisedText>
</PrefetchLink>
</motion.div>))}
<PrefetchLink
@@ -396,7 +407,7 @@ const TopNav: FC<Props> = ({ user, locales }) => {
className={cn ('top-nav-menu-link relative z-10 flex h-full items-center',
'shrink-0 whitespace-nowrap',
(openItemIdx < 0 || moreVsbl) && 'font-bold')}>
その他 &raquo;
<TranslatedText messageKey="more"/>
</PrefetchLink>
</div>
</div>
@@ -452,7 +463,11 @@ const TopNav: FC<Props> = ({ user, locales }) => {
animate: { x: 0, y: 0, opacity: 1 },
exit: { x: 40, y: -40, opacity: 0 } }))}
className="z-10 h-full flex shrink-0 items-center px-3 font-bold w-28">
<h2>{item.name}</h2>
<h2>
<LocalisedText language={item.nameLanguage ?? 'ja'}>
{item.name}
</LocalisedText>
</h2>
</motion.div>
{item.subMenu
.filter (subItem => subItem.visible ?? true)
@@ -489,7 +504,9 @@ const TopNav: FC<Props> = ({ user, locales }) => {
target={subItem.to.slice (0, 2) === '//' ? '_blank' : undefined}
onClick={() => setMoreVsbl (false)}
className="top-nav-menu-link h-full flex items-center px-3">
{subItem.name}
<LocalisedText language={subItem.nameLanguage ?? 'ja'}>
{subItem.name}
</LocalisedText>
</PrefetchLink>
</motion.div>)))}
</div>
@@ -517,7 +534,9 @@ const TopNav: FC<Props> = ({ user, locales }) => {
? '_blank'
: undefined}
className="top-nav-menu-link h-full flex items-center px-3">
{item.name}
<LocalisedText language={item.nameLanguage ?? 'ja'}>
{item.name}
</LocalisedText>
</PrefetchLink>
</div>)))}
</div>)
@@ -557,7 +576,9 @@ const TopNav: FC<Props> = ({ user, locales }) => {
? '_blank'
: undefined}
className="top-nav-menu-link h-full flex items-center px-3">
{item.name}
<LocalisedText language={item.nameLanguage ?? 'ja'}>
{item.name}
</LocalisedText>
</PrefetchLink>
</motion.div>)))}
</motion.div>
@@ -587,7 +608,9 @@ const TopNav: FC<Props> = ({ user, locales }) => {
setOpenItemIdx (i)
}
}}>
{item.name}
<LocalisedText language={item.nameLanguage ?? 'ja'}>
{item.name}
</LocalisedText>
</PrefetchLink>
{i === openItemIdx && (
@@ -608,7 +631,9 @@ const TopNav: FC<Props> = ({ user, locales }) => {
? '_blank'
: undefined}
className="top-nav-mobile-row w-full min-h-[36px] flex items-center pl-12">
{subItem.name}
<LocalisedText language={subItem.nameLanguage ?? 'ja'}>
{subItem.name}
</LocalisedText>
</PrefetchLink>)))}
</div>)}
</Fragment>))}
@@ -617,7 +642,7 @@ const TopNav: FC<Props> = ({ user, locales }) => {
className={cn ('top-nav-mobile-row w-full min-h-[40px] flex items-center pl-8',
((openItemIdx < 0)
&& 'top-nav-mobile-active font-bold'))}>
その他 &raquo;
<TranslatedText messageKey="more"/>
</PrefetchLink>
<TopNavUser user={user} sp/>
<Separator/>
@@ -652,7 +677,9 @@ const TopNav: FC<Props> = ({ user, locales }) => {
setOpenItemIdx (i)
}
}}>
{item.name}
<LocalisedText language={item.nameLanguage ?? 'ja'}>
{item.name}
</LocalisedText>
</PrefetchLink>
<AnimatePresence initial={false}>
@@ -686,7 +713,10 @@ const TopNav: FC<Props> = ({ user, locales }) => {
? '_blank'
: undefined}
className="top-nav-mobile-row w-full min-h-[36px] flex items-center pl-12">
{subItem.name}
<LocalisedText
language={subItem.nameLanguage ?? 'ja'}>
{subItem.name}
</LocalisedText>
</PrefetchLink>)))}
</motion.div>)}
</AnimatePresence>
@@ -696,7 +726,7 @@ const TopNav: FC<Props> = ({ user, locales }) => {
className={cn ('top-nav-mobile-row w-full min-h-[40px] flex items-center pl-8',
((openItemIdx < 0)
&& 'top-nav-mobile-active font-bold'))}>
その他 &raquo;
<TranslatedText messageKey="more"/>
</PrefetchLink>
<TopNavUser user={user} sp/>
<Separator/>
+26
ファイルの表示
@@ -0,0 +1,26 @@
import { useTranslation } from 'react-i18next'
import LocalisedText from '@/components/LocalisedText'
import { useLocale } from '@/lib/useLocale'
type Props = {
messageKey: string
namespace?: string
values?: Record<string, string | number>
className?: string }
const TranslatedText = (
{ messageKey, namespace = 'navigation', values, className }: Props,
) => {
const locale = useLocale ()
const { t } = useTranslation (namespace, { lng: locale })
const result = t (messageKey, { ...values, returnDetails: true })
return (
<LocalisedText language={result.usedLng} className={className}>
{result.res}
</LocalisedText>)
}
export default TranslatedText