AI: address issue #455

このコミットが含まれているのは:
Codex Night Worker
2026-11-14 00:32:39 +09:00
コミット 0d369618f4
15個のファイルの変更、152行の追加、61行の削除
+1 -1
ファイルの表示
@@ -7,5 +7,5 @@ Font copyright: Copyright 2018 Google Inc. All Rights Reserved.
- License: SIL Open Font License 1.1; see the accompanying LICENSE.
- Retrieved: 2026-11-08.
Used only by the Unicode Mongolian sample in the development layout route.
Used by the Mongolian UI at `/mn-Mong`.
The font and license are served together from this directory.
+15 -9
ファイルの表示
@@ -26,7 +26,7 @@ import { isAuthorityDependentQuery, localesKeys, sessionKeys } from '@/lib/query
import { isCountsPath } from '@/lib/routeLocales'
import { switchPostFilterLocale } from '@/lib/posts'
import { switchWikiLocale } from '@/lib/wiki'
import { useLocale, useVerticalPreview } from '@/lib/useLocale'
import { useLocale, useUILocale, useVerticalPreview } from '@/lib/useLocale'
import { applyClientAnimationMode,
applyClientAppearance,
fetchUserThemeSlots,
@@ -46,7 +46,7 @@ import ServiceUnavailable from '@/pages/ServiceUnavailable'
import type { ComponentType, Dispatch, FC, ReactNode, SetStateAction } from 'react'
import type { RouteMatch } from 'react-router-dom'
import type { AvailableLocale, LocaleCode } from '@/lib/locales'
import type { AvailableLocale, LocaleCode, UILocaleCode } from '@/lib/locales'
import type { PageRoute } from '@/lib/routeLocales'
import type { User } from '@/types'
@@ -272,11 +272,16 @@ const RoutedApp = ({ locales }: { locales: AvailableLocale[] }) => {
const robots = isCountsPath (location.pathname) ? 'noindex' : route?.route.handle?.robots
const pagePath = unlocalisedPath (location.pathname)
const countsPage = pagePath === '/counts' || pagePath.startsWith ('/counts/')
const switchLocale = async (targetLocale: LocaleCode): Promise<string> => {
const switchLocale = async (target: UILocaleCode): Promise<string> => {
const targetLocale = target === 'mn-Mong' ? 'ja' : target
if (targetLocale === locale)
return switchLocalePath (location, target)
const resolve = route?.route.handle?.switchLocale
return (resolve != null && route != null)
? resolve ({ location, params: route.params, locale, targetLocale })
const destination =
(resolve != null && route != null)
? await resolve ({ location, params: route.params, locale, targetLocale })
: switchLocalePath (location, targetLocale)
return target === 'mn-Mong' ? `/mn-Mong${unlocalisedPath (destination)}` : destination
}
const [user, setUserState] = useState<User | null> (
() => client.getQueryData<User> (sessionKeys.current) ?? null)
@@ -413,6 +418,7 @@ const RoutedApp = ({ locales }: { locales: AvailableLocale[] }) => {
const LocaleRoute: FC = () => {
const { locale } = useParams ()
const uiLocale = useUILocale ()
const location = useLocation ()
const verticalPreview = isVerticalPreviewPath (location.pathname)
const { i18n } = useTranslation ()
@@ -433,10 +439,10 @@ const LocaleRoute: FC = () => {
useLayoutEffect (() => {
if (activeLocale != null)
document.documentElement.lang = activeLocale
if (activeLocale != null && i18n.language !== activeLocale)
void i18n.changeLanguage (activeLocale)
}, [activeLocale, i18n])
document.documentElement.lang = uiLocale
if (activeLocale != null && i18n.language !== uiLocale)
void i18n.changeLanguage (uiLocale)
}, [activeLocale, uiLocale, i18n])
const redirect = legacyLocalePath (location, pathname => resolvePageRoute (pathname) != null)
if (!(isLocaleCode (locale)) && !verticalPreview && redirect == null)
+20 -22
ファイルの表示
@@ -4,7 +4,7 @@ import { Globe } from 'lucide-react'
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate, useVerticalPreview } from '@/lib/useLocale'
import { useLocale, useLocaleNavigate, useUILocale, useVerticalPreview } from '@/lib/useLocale'
import LocalisedText, { formatUIString } from '@/components/LocalisedText'
import Separator from '@/components/MenuSeparator'
import PrefetchLink from '@/components/PrefetchLink'
@@ -13,8 +13,10 @@ import TranslatedText from '@/components/TranslatedText'
import { toast } from '@/components/ui/use-toast'
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
import { isLocaleCode,
localeFromPath,
isUILocaleCode,
MONGOLIAN_LOCALE,
switchLocalePath,
uiLocaleFromPath,
unlocalisedPath } from '@/lib/locales'
import { uiMessage, uiMessageLanguage } from '@/lib/messages'
import { useUITranslation } from '@/lib/useUITranslation'
@@ -28,13 +30,13 @@ import { fetchWikiPage } from '@/lib/wiki'
import type { FC, MouseEvent } from 'react'
import type { AvailableLocale, LocaleCode } from '@/lib/locales'
import type { AvailableLocale, UILocaleCode } from '@/lib/locales'
import type { Material, Menu, MenuVisibleItem, Tag, User } from '@/types'
type Props = {
user: User | null
locales: AvailableLocale[]
resolveLocaleSwitch?: (locale: LocaleCode) => Promise<string> }
resolveLocaleSwitch?: (locale: UILocaleCode) => Promise<string> }
type NavDensity = 'auto' | 'comfortable' | 'compact'
@@ -47,7 +49,7 @@ export const menuOutline = (
pathName: string },
): Menu => {
const pathName = unlocalisedPath (fullPathname)
const locale = localeFromPath (fullPathname) ?? 'ja'
const locale = uiLocaleFromPath (fullPathname)
const message = (
key: Parameters<typeof uiMessage>[1],
@@ -137,8 +139,11 @@ export const menuOutline = (
const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
const { message, label } = useUITranslation ('shared')
const locale = useLocale ()
const uiLocale = useUILocale ()
const verticalPreview = useVerticalPreview ()
const localeName = locales.find (entry => entry.code === locale)?.name
const uiLocales = [...locales.filter (entry => isLocaleCode (entry.code)),
...(locales.some (entry => entry.code === 'ja') ? [MONGOLIAN_LOCALE] : [])]
const localeName = uiLocales.find (entry => entry.code === uiLocale)?.name
const navigate = useLocaleNavigate ()
const location = useLocation ()
const currentLocation = useRef (location)
@@ -443,17 +448,18 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
rounded px-1 focus-within:ring-2">
<Globe aria-hidden="true" size={16}/>
<span className="top-nav-locale-name whitespace-nowrap text-sm">
<LocalisedText language={locale}>{localeName}</LocalisedText>
<LocalisedText language={uiLocale}>{localeName}</LocalisedText>
</span>
<select
aria-label={formatUIString (localeName ?? '', locale)}
value={locale}
aria-label={formatUIString (localeName ?? '', uiLocale)}
lang={uiLocale}
value={uiLocale}
disabled={switchingLocale}
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
onChange={async event => {
const nextLocale = event.target.value
if (isLocaleCode (nextLocale)
&& locales.some (entry => entry.code === nextLocale))
if (isUILocaleCode (nextLocale)
&& uiLocales.some (entry => entry.code === nextLocale))
{
const request = ++localeSwitchRequest.current
setSwitchingLocale (true)
@@ -478,8 +484,8 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
}
}
}}>
{locales.map (entry => isLocaleCode (entry.code) && (
<option key={entry.code} value={entry.code}>
{uiLocales.map (entry => (
<option key={entry.code} value={entry.code} lang={entry.code}>
{formatUIString (entry.name, entry.code)}
</option>))}
</select>
@@ -489,7 +495,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
<div ref={layoutRef} className="top-nav-layout w-full min-w-0 shrink-0"
data-desktop={desktop ?? undefined} data-density={density}
data-layout-ready={layoutReady}>
<nav ref={barRef} lang={uiMessageLanguage (locale, 'posts')}
<nav ref={barRef} lang={uiMessageLanguage (uiLocale, '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
@@ -560,18 +566,10 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
<div className="top-nav-account ml-auto flex min-w-0 items-center gap-2">
<TopNavUser user={user}/>
{localeSelector}
{verticalPreview && (
<span className="vertical-preview-sample" lang="mn-Mong">
ᠮᠣᠩᠭᠣᠯ
</span>)}
</div>
<div className="top-nav-mobile-controls ml-auto flex shrink-0 items-center gap-2">
{menuOpen && localeSelector}
{verticalPreview && (
<span className="vertical-preview-sample" lang="mn-Mong">
ᠮᠣᠩᠭᠣᠯ
</span>)}
<button
type="button"
className="top-nav-mobile-toggle shrink-0 whitespace-nowrap
+1 -1
ファイルの表示
@@ -8,7 +8,7 @@ type Props = {
const Form: FC<Props> = ({ children, className }) => (
<div className={cn ('mx-auto max-w-xl space-y-4 p-4', className)}>
<div className={cn ('form-layout mx-auto max-w-xl space-y-4 p-4', className)}>
{children}
</div>)
+1 -1
ファイルの表示
@@ -17,7 +17,7 @@ type Props = {
const MainArea: FC<Props> = (
{ children, className, animateLayout = true, verticalContent = false },
{ children, className, animateLayout = true, verticalContent = true },
) => {
const behaviourSettings = useClientBehaviourSettings ()
const verticalPreview = useVerticalPreview ()
+5 -4
ファイルの表示
@@ -10,7 +10,7 @@ import { AppFrame, appRoutes, resolvePageRoute } from '@/App'
import { apiGet, isApiError } from '@/lib/api'
import { createUII18n } from '@/lib/i18n'
import { fetchLocales, isLocaleCode, isVerticalPreviewPath,
legacyLocalePath } from '@/lib/locales'
legacyLocalePath, uiLocaleFromPath } from '@/lib/locales'
import { localesKeys, sessionKeys } from '@/lib/queryKeys'
import { withRenderContext } from '@/lib/renderContext.server'
@@ -78,6 +78,7 @@ export const render = async (
if (legacy != null && known != null)
return { status: 308, location: legacy }
const locale = isLocaleCode (segment) ? segment : 'ja'
const uiLocale = uiLocaleFromPath (pathname)
const routePrefix = verticalPreview ? 'mn-Mong' : locale
if (known?.route.handle?.redirectTo != null)
return { status: 308,
@@ -117,7 +118,7 @@ export const render = async (
if (prepare != null && known != null && status === 200)
status = await prepare ({ client, params: known.params, locale,
searchParams: url.searchParams })
const instance = createUII18n (locale)
const instance = createUII18n (uiLocale)
const handler = createStaticHandler (appRoutes)
const context = await handler.query (new Request (url))
if (context instanceof Response)
@@ -187,11 +188,11 @@ export const render = async (
isAxiosError: true, response: {
status: isApiError (query.state.error) ? query.state.error.response?.status : 500 } } }
})) }
const bootstrap = JSON.stringify ({ state, status, locale,
const bootstrap = JSON.stringify ({ state, status, locale: uiLocale,
url: url.pathname + url.search })
.replace (/</g, '\\u003c').replace (/\u2028/g, '\\u2028')
.replace (/\u2029/g, '\\u2029')
return { status, html, head, locale, bootstrap,
return { status, html, head, locale: uiLocale, bootstrap,
setCookies: requestContext.setCookies }
}
finally
+66 -5
ファイルの表示
@@ -626,17 +626,22 @@ body
will-change: transform;
}
/* Loaded only by the development-only sample, with the OFL alongside the font. */
/* Locally hosted Mongolian font; the OFL accompanies the font file. */
@font-face
{
font-family: 'BTRC Mongolian Preview';
font-family: 'BTRC Mongolian';
src: url('/fonts/noto-sans-mongolian/NotoSansMongolian-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* Development-only /mn-Mong layout. Unannotated pages retain horizontal text. */
/* Mongolian flows down each line, then into the next column on the right. */
:lang(mn-Mong)
{
font-family: 'BTRC Mongolian', 'Mongolian Baiti', 'Noto Sans JP Variable', sans-serif;
}
[data-vertical-preview]
{
min-width: 0;
@@ -675,6 +680,56 @@ body
overscroll-behavior-x: contain;
}
/* Specialised lists below retain their own grid and table layouts. */
[data-vertical-preview] .main-area[data-vertical-content]:not(
.posts-main-area, .tags-main-area, .wiki-main-area)
{
writing-mode: vertical-lr;
text-orientation: mixed;
text-align: start;
overflow-y: auto;
}
[data-vertical-preview] .main-area[data-vertical-content]:not(
.posts-main-area, .tags-main-area, .wiki-main-area) > :is(div, article, section)
{
max-width: none;
max-inline-size: 100%;
}
[data-vertical-preview] .main-area[data-vertical-content] .prose
{
max-width: none;
max-inline-size: 100%;
}
/* Input and data widgets keep their physical coordinates and nested scrolling. */
[data-vertical-preview] .main-area[data-vertical-content] :is(
form, .form-layout, pre, .overflow-x-auto, .rc-md-editor, .not-prose)
{
writing-mode: horizontal-tb;
text-orientation: mixed;
max-height: 100%;
overflow: auto;
}
[data-vertical-preview] .main-area[data-vertical-content] :is(form, .form-layout)
{
width: min(36rem, calc(100vw - 2rem));
}
[data-vertical-preview] :where(.main-area[data-vertical-content]) :is(
button, input, select, textarea, code, time, table, img, video, iframe, canvas, svg)
{
writing-mode: horizontal-tb;
}
[data-vertical-preview] .main-area[data-vertical-content] :is(img, video, iframe, canvas)
{
max-height: 100%;
object-fit: contain;
}
[data-vertical-preview] .sidebar-panel
{
width: 100%;
@@ -741,16 +796,22 @@ body
height: 1px;
}
[data-vertical-preview] .vertical-preview-sample
[data-vertical-preview] .top-nav-locale-name:has(:lang(mn-Mong))
{
display: inline;
writing-mode: vertical-lr;
text-orientation: mixed;
font-family: 'BTRC Mongolian Preview', 'Mongolian Baiti', sans-serif;
font-size: 1.25rem;
line-height: 1.5;
flex-shrink: 0;
}
[data-vertical-preview] .top-nav-menu-link:has(.top-nav-locale-name)
{
height: auto;
min-height: 2rem;
}
/* The post grid fills each column downwards before continuing to the right. */
[data-vertical-preview] .posts-main-area > div
{
+11 -4
ファイルの表示
@@ -1,9 +1,11 @@
import { createInstance } from 'i18next'
import { initReactI18next } from 'react-i18next'
import { MONGOLIAN_LOCALE, SUPPORTED_LOCALES } from '@/lib/locales'
import type { i18n as I18n, TOptions } from 'i18next'
import type { LocaleCode } from '@/lib/locales'
import type { UILocaleCode } from '@/lib/locales'
// Keep Japanese placeholders editable without reporting them as completed translations.
const translatedResource = (resource: Record<string, unknown>): Record<string, unknown> => {
@@ -34,9 +36,14 @@ for (const [path, dictionary] of Object.entries (dictionaries))
const [, , locale, filename] = path.split ('/')
resources[locale] ??= { }
resources[locale][filename.slice (0, -5)] =
locale === 'ain-Kana' ? translatedResource (dictionary) : dictionary
locale === 'ja' ? dictionary : translatedResource (dictionary)
}
const namespaces = Object.keys (resources.ja ?? { })
// No Mongolian translations yet; preserve the existing fallback language metadata.
resources[MONGOLIAN_LOCALE.code] ??= { }
for (const namespace of namespaces)
resources[MONGOLIAN_LOCALE.code][namespace] ??=
translatedResource ({ _untranslated: ['*'] })
export const createUII18n = (locale: string): I18n => {
@@ -44,7 +51,7 @@ export const createUII18n = (locale: string): I18n => {
void instance.init ({
lng: locale,
fallbackLng: 'ja',
supportedLngs: ['ja', 'ain-Kana'],
supportedLngs: [...SUPPORTED_LOCALES, MONGOLIAN_LOCALE.code],
load: 'currentOnly',
initAsync: false,
defaultNS: 'navigation',
@@ -122,7 +129,7 @@ export const languageUIMessage = (
export const untranslatedKeys = (
locale: LocaleCode,
locale: UILocaleCode,
namespace = 'navigation',
): string[] => {
const collect = (resource: Record<string, unknown>, prefix = ''): string[] =>
+15 -3
ファイルの表示
@@ -7,6 +7,10 @@ export const SUPPORTED_LOCALES = ['ja', 'ain-Kana'] as const
export type LocaleCode = typeof SUPPORTED_LOCALES[number]
// This UI locale uses Japanese content until the API supports Mongolian.
export const MONGOLIAN_LOCALE = { code: 'mn-Mong', name: 'ᠮᠣᠩᠭᠣᠯ' } as const
export type UILocaleCode = LocaleCode | typeof MONGOLIAN_LOCALE.code
export type AvailableLocale = { code: string; name: string }
@@ -28,9 +32,17 @@ export const localeFromPath = (pathname: string): LocaleCode | null => {
}
// A development-only layout route; API and dictionary locales stay unchanged.
// Frontend layout selection must not change the API's content locale.
export const isVerticalPreviewPath = (pathname: string): boolean =>
import.meta.env.DEV && /^\/mn-Mong(?:[/?#]|$)/.test (pathname)
/^\/mn-Mong(?:[/?#]|$)/.test (pathname)
export const isUILocaleCode = (value: unknown): value is UILocaleCode =>
value === MONGOLIAN_LOCALE.code || isLocaleCode (value)
export const uiLocaleFromPath = (pathname: string): UILocaleCode =>
isVerticalPreviewPath (pathname) ? MONGOLIAN_LOCALE.code : localeFromPath (pathname) ?? 'ja'
export const resolveViewingLocale = (
@@ -67,7 +79,7 @@ export const localiseTo = (to: To, locale: LocaleCode, viewingPath = ''): To =>
export const switchLocalePath = (
location: { pathname: string; search: string; hash: string },
locale: LocaleCode,
locale: UILocaleCode,
): string =>
`/${ locale }${ unlocalisedPath (location.pathname) }${ location.search }${ location.hash }`
+3 -3
ファイルの表示
@@ -1,19 +1,19 @@
import { translateUIMessage } from '@/lib/i18n'
import type navigation from '@/locales/ja/navigation.json'
import type { LocaleCode } from '@/lib/locales'
import type { UILocaleCode } from '@/lib/locales'
export type UIMessageKey = keyof typeof navigation
export const uiMessage = (
locale: LocaleCode,
locale: UILocaleCode,
key: UIMessageKey,
values: Record<string, string | number> = { },
): string => translateUIMessage (key, 'navigation', locale, values).text
export const uiMessageLanguage = (locale: LocaleCode, key: UIMessageKey): 'ja' | 'ain' =>
export const uiMessageLanguage = (locale: UILocaleCode, key: UIMessageKey): 'ja' | 'ain' =>
translateUIMessage (key, 'navigation', locale).language === 'ain-Kana'
? 'ain'
: 'ja'
+7 -2
ファイルの表示
@@ -1,11 +1,12 @@
import { useCallback } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { isVerticalPreviewPath, localiseTo, resolveViewingLocale } from '@/lib/locales'
import { isVerticalPreviewPath, localiseTo,
resolveViewingLocale, uiLocaleFromPath } from '@/lib/locales'
import type { NavigateFunction, NavigateOptions, To } from 'react-router-dom'
import type { LocaleCode } from '@/lib/locales'
import type { LocaleCode, UILocaleCode } from '@/lib/locales'
export const useLocale = (): LocaleCode => {
@@ -15,6 +16,10 @@ export const useLocale = (): LocaleCode => {
}
export const useUILocale = (): UILocaleCode =>
uiLocaleFromPath (useLocation ().pathname)
export const useLocaleNavigate = (): NavigateFunction => {
const navigate = useNavigate ()
const locale = useLocale ()
+2 -2
ファイルの表示
@@ -3,11 +3,11 @@ import { useTranslation } from 'react-i18next'
import LocalisedText from '@/components/LocalisedText'
import { translateUIMessage } from '@/lib/i18n'
import { useLocale } from '@/lib/useLocale'
import { useUILocale } from '@/lib/useLocale'
export const useUITranslation = (namespace = 'common') => {
const locale = useLocale ()
const locale = useUILocale ()
const { i18n } = useTranslation (namespace, { lng: locale })
const message = useCallback ((key: string, values?: Record<string, string | number>) => {
+2 -2
ファイルの表示
@@ -6,7 +6,7 @@ import { I18nextProvider } from 'react-i18next'
import '@/index.css'
import App, { preloadPageRoute, resolvePageRoute } from '@/App'
import { createUII18n } from '@/lib/i18n'
import { currentViewingLocale } from '@/lib/locales'
import { currentViewingLocale, uiLocaleFromPath } from '@/lib/locales'
import { localesKeys } from '@/lib/queryKeys'
import { applyClientAnimationMode, getClientAnimationMode } from '@/lib/settings'
@@ -22,7 +22,7 @@ const client = new QueryClient ({
const mount = (serverRendered: boolean) => {
const instance = createUII18n (currentViewingLocale ())
const instance = createUII18n (uiLocaleFromPath (window.location.pathname))
const app = (
<HelmetProvider context={helmetContext}>
<QueryClientProvider client={client}>
+2 -1
ファイルの表示
@@ -359,7 +359,8 @@ const CountsResourceContent = (
</ul>
</nav>
</SidebarComponent>
<MainArea animateLayout={false} className="flex min-h-0 min-w-0 flex-col p-2">
<MainArea animateLayout={false} verticalContent={false}
className="flex min-h-0 min-w-0 flex-col p-2">
<PageTitle className="mb-1 text-base" lang={title.language}>{title.text}</PageTitle>
<div ref={controlsArea} className="shrink-0"
style={{ minHeight: definition == null ? controlsHeight : undefined }}>
+1 -1
ファイルの表示
@@ -107,7 +107,7 @@ const PostDetailPage: FC<Props> = ({ user }) => {
{post && <TagDetailSidebar post={post} className="hidden md:block"/>}
<MainArea className="relative">
<MainArea className="relative" verticalContent={false}>
{post
? (
<>