このコミットが含まれているのは:
@@ -1,5 +1,6 @@
|
||||
class PostVersionsController < ApplicationController
|
||||
def index
|
||||
locale = resolve_locale!
|
||||
post_id = params[:post].presence
|
||||
tag_id = params[:tag].presence&.to_i
|
||||
external_tag_id = params[:external_tag].presence&.to_i
|
||||
@@ -45,12 +46,12 @@ class PostVersionsController < ApplicationController
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
render json: { versions: serialise_versions(versions), count: }
|
||||
render json: { versions: serialise_versions(versions, locale.language_code), count: }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialise_versions rows
|
||||
def serialise_versions rows, language_code
|
||||
rows = rows.to_a
|
||||
user_ids = rows.map(&:created_by_user_id).compact.uniq
|
||||
users_by_id = User.where(id: user_ids).pluck(:id, :name).to_h
|
||||
@@ -58,12 +59,16 @@ class PostVersionsController < ApplicationController
|
||||
[normalise_json(row.tags_json),
|
||||
normalise_json(row.attributes['prev_tags_json']) || []]
|
||||
}
|
||||
tag_ids = snapshots.flatten.filter_map { _1['tag_id'] }.uniq
|
||||
tag_names = TagName.where(tag_id: tag_ids, language_code:, primary_flg: true)
|
||||
.pluck(:tag_id, :name).to_h
|
||||
external_tag_names = external_tag_names_for(snapshots)
|
||||
|
||||
rows.map do |row|
|
||||
cur_tags = snapshot_tag_literals(normalise_json(row.tags_json), external_tag_names)
|
||||
cur_tags = snapshot_tag_literals(
|
||||
normalise_json(row.tags_json), tag_names, external_tag_names)
|
||||
prev_tags = snapshot_tag_literals(
|
||||
normalise_json(row.attributes['prev_tags_json']) || [], external_tag_names)
|
||||
normalise_json(row.attributes['prev_tags_json']) || [], tag_names, external_tag_names)
|
||||
|
||||
{ post_id: row.post_id,
|
||||
version_no: row.version_no,
|
||||
@@ -106,17 +111,23 @@ class PostVersionsController < ApplicationController
|
||||
names
|
||||
end
|
||||
|
||||
def snapshot_tag_literals snapshots, external_tag_names
|
||||
def snapshot_tag_literals snapshots, tag_names, external_tag_names
|
||||
snapshots
|
||||
.filter_map { |snapshot|
|
||||
if snapshot.key?('tag_id')
|
||||
sections = snapshot.fetch('sections', []).map { |section|
|
||||
[section.fetch('begin_ms'), section['end_ms']]
|
||||
}.sort_by { |begin_ms, end_ms| [begin_ms, end_ms || Float::INFINITY] }
|
||||
name = tag_names[snapshot['tag_id']] || snapshot.fetch('name')
|
||||
[tag_category_order(snapshot['category']),
|
||||
Post.tag_snapshot_literal(snapshot)]
|
||||
{ identity: [:tag, snapshot['tag_id'], sections],
|
||||
name: Post.tag_snapshot_literal(snapshot.merge('name' => name)) }]
|
||||
elsif external_tag_names[snapshot['external_tag_id']]
|
||||
[6, external_tag_names[snapshot['external_tag_id']]]
|
||||
[6, { identity: [:external_tag, snapshot['external_tag_id']],
|
||||
name: external_tag_names[snapshot['external_tag_id']] }]
|
||||
end
|
||||
}
|
||||
.sort_by { |order, name| [order, name.downcase] }
|
||||
.sort_by { |order, tag| [order, tag[:name].downcase] }
|
||||
.map(&:second)
|
||||
end
|
||||
|
||||
@@ -125,17 +136,20 @@ class PostVersionsController < ApplicationController
|
||||
end
|
||||
|
||||
def build_version_tags cur_tags, prev_tags
|
||||
(cur_tags | prev_tags).map do |name|
|
||||
current = cur_tags.to_h { [_1[:identity], _1[:name]] }
|
||||
previous = prev_tags.to_h { [_1[:identity], _1[:name]] }
|
||||
|
||||
(current.keys | previous.keys).map do |identity|
|
||||
type =
|
||||
if cur_tags.include?(name) && prev_tags.include?(name)
|
||||
if current.key?(identity) && previous.key?(identity)
|
||||
'context'
|
||||
elsif cur_tags.include?(name)
|
||||
elsif current.key?(identity)
|
||||
'added'
|
||||
else
|
||||
'removed'
|
||||
end
|
||||
|
||||
{ name:, type: }
|
||||
{ name: current[identity] || previous[identity], type: }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
生成ファイル
+78
-3
@@ -25,12 +25,14 @@
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.23.26",
|
||||
"humps": "^2.0.1",
|
||||
"i18next": "^26.4.7",
|
||||
"lucide-react": "^0.511.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"path-to-regexp": "^8.3.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-i18next": "^17.0.20",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-markdown-editor-lite": "^1.3.4",
|
||||
"react-router-dom": "^6.30.1",
|
||||
@@ -350,9 +352,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.27.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
|
||||
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
|
||||
"version": "7.29.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz",
|
||||
"integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -4872,6 +4874,15 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-4.0.1.tgz",
|
||||
"integrity": "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://locize.com"
|
||||
}
|
||||
},
|
||||
"node_modules/html-url-attributes": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
||||
@@ -4916,6 +4927,34 @@
|
||||
"integrity": "sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "26.4.7",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.7.tgz",
|
||||
"integrity": "sha512-p/3hqy6EcuqRs6wNwBYwGGC1fzBJSMxV9zMuN814ab/7QjdZbXfjXwuPZdQbngX6KDOTxM01uUiZyqmMB8ghZg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.locize.com/i18next"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.locize.com"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": "^5 || ^6 || ^7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -7258,6 +7297,33 @@
|
||||
"react": "^16.6.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "17.0.20",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.20.tgz",
|
||||
"integrity": "sha512-rzSSirf0NP9vfcUXHZAksIuyMWl7L/T/7nm8S+sSIFuIzku54qcyyVAOZXbawPNpUxGkDx/eosSv4DrpzcxuOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.7",
|
||||
"html-parse-stringify": "^4.0.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 26.2.0",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5 || ^6 || ^7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -8595,6 +8661,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz",
|
||||
"integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
||||
@@ -29,12 +29,14 @@
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.23.26",
|
||||
"humps": "^2.0.1",
|
||||
"i18next": "^26.4.7",
|
||||
"lucide-react": "^0.511.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"path-to-regexp": "^8.3.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-i18next": "^17.0.20",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-markdown-editor-lite": "^1.3.4",
|
||||
"react-router-dom": "^6.30.1",
|
||||
|
||||
+23
-2
@@ -1,6 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion'
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react'
|
||||
import { Fragment, useEffect, useLayoutEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { createBrowserRouter,
|
||||
Navigate,
|
||||
Route,
|
||||
@@ -17,6 +18,7 @@ import { Toaster } from '@/components/ui/toaster'
|
||||
import { apiPost, isApiError } from '@/lib/api'
|
||||
import { fetchLocales, isLocaleCode, legacyLocalePath, switchLocalePath } from '@/lib/locales'
|
||||
import { localesKeys } from '@/lib/queryKeys'
|
||||
import { requiredPageLocale } from '@/lib/routeLocales'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import { applyClientAnimationMode,
|
||||
applyClientAppearance,
|
||||
@@ -41,7 +43,7 @@ import MorePage from '@/pages/MorePage'
|
||||
import GekanatorPage from '@/pages/GekanatorPage'
|
||||
import NicoTagListPage from '@/pages/tags/NicoTagListPage'
|
||||
import NotFound from '@/pages/NotFound'
|
||||
import TOSPage from '@/pages/TOSPage.mdx'
|
||||
import TOSPage from '@/pages/TOSPage'
|
||||
import HelpDetailPage from '@/pages/help/HelpDetailPage'
|
||||
import HelpChangesPage from '@/pages/help/HelpChangesPage'
|
||||
import HelpDiffPage from '@/pages/help/HelpDiffPage'
|
||||
@@ -268,6 +270,7 @@ const RoutedApp = ({ locales }: { locales: AvailableLocale[] }) => {
|
||||
const LocaleRoute: FC = () => {
|
||||
const { locale } = useParams ()
|
||||
const location = useLocation ()
|
||||
const { i18n } = useTranslation ()
|
||||
const { data, isPending, isError, refetch } = useQuery ({
|
||||
queryKey: localesKeys.index,
|
||||
queryFn: fetchLocales,
|
||||
@@ -275,6 +278,18 @@ const LocaleRoute: FC = () => {
|
||||
refetchOnMount: 'always',
|
||||
refetchOnWindowFocus: 'always',
|
||||
refetchOnReconnect: 'always' })
|
||||
const pageLocale = requiredPageLocale (location.pathname)
|
||||
const activeLocale =
|
||||
(isLocaleCode (locale)
|
||||
&& data?.locales.some (entry => entry.code === locale)
|
||||
&& (pageLocale == null || pageLocale === locale))
|
||||
? locale
|
||||
: null
|
||||
|
||||
useLayoutEffect (() => {
|
||||
if (activeLocale != null && i18n.language !== activeLocale)
|
||||
void i18n.changeLanguage (activeLocale)
|
||||
}, [activeLocale, i18n])
|
||||
|
||||
useEffect (() => {
|
||||
void refetch ()
|
||||
@@ -287,6 +302,12 @@ const LocaleRoute: FC = () => {
|
||||
return <ServiceUnavailable/>
|
||||
if (isPending)
|
||||
return null
|
||||
if (pageLocale != null && isLocaleCode (locale) && pageLocale !== locale)
|
||||
{
|
||||
return data.locales.some (entry => entry.code === pageLocale)
|
||||
? <Navigate to={switchLocalePath (location, pageLocale)} replace/>
|
||||
: <NotFound/>
|
||||
}
|
||||
if (isLocaleCode (locale))
|
||||
{
|
||||
return data.locales.some (entry => entry.code === locale)
|
||||
|
||||
@@ -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
|
||||
@@ -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')}>
|
||||
その他 »
|
||||
<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'))}>
|
||||
その他 »
|
||||
<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'))}>
|
||||
その他 »
|
||||
<TranslatedText messageKey="more"/>
|
||||
</PrefetchLink>
|
||||
<TopNavUser user={user} sp/>
|
||||
<Separator/>
|
||||
|
||||
@@ -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
|
||||
@@ -4,6 +4,20 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.ainu-ui-text
|
||||
{
|
||||
white-space: normal !important;
|
||||
word-break: keep-all !important;
|
||||
overflow-wrap: normal !important;
|
||||
}
|
||||
|
||||
.ainu-ui-word
|
||||
{
|
||||
white-space: nowrap !important;
|
||||
word-break: keep-all !important;
|
||||
overflow-wrap: normal !important;
|
||||
}
|
||||
|
||||
@layer base
|
||||
{
|
||||
:root
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { createInstance } from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
|
||||
import ainuNavigation from '@/locales/ain-Kana/navigation.json'
|
||||
import nipponeseNavigation from '@/locales/ja/navigation.json'
|
||||
|
||||
import type { LocaleCode } from '@/lib/locales'
|
||||
|
||||
export const i18n = createInstance ()
|
||||
|
||||
void i18n.use (initReactI18next).init ({
|
||||
lng: 'ja',
|
||||
fallbackLng: 'ja',
|
||||
supportedLngs: ['ja', 'ain-Kana'],
|
||||
load: 'currentOnly',
|
||||
initAsync: false,
|
||||
defaultNS: 'navigation',
|
||||
ns: ['navigation'],
|
||||
resources: {
|
||||
ja: { navigation: nipponeseNavigation },
|
||||
'ain-Kana': { navigation: ainuNavigation } },
|
||||
interpolation: { escapeValue: false },
|
||||
returnNull: false,
|
||||
returnEmptyString: false,
|
||||
saveMissing: import.meta.env.DEV,
|
||||
missingKeyHandler: (languages, namespace, key) => {
|
||||
console.warn ('[i18n] missing key', { languages, namespace, key })
|
||||
},
|
||||
react: { useSuspense: false } })
|
||||
|
||||
|
||||
export const untranslatedKeys = (
|
||||
locale: LocaleCode,
|
||||
namespace = 'navigation',
|
||||
): string[] => {
|
||||
const collect = (resource: Record<string, unknown>, prefix = ''): string[] =>
|
||||
Object.entries (resource).flatMap (([key, value]) => {
|
||||
const path = prefix ? `${ prefix }.${ key }` : key
|
||||
return (value != null && typeof value === 'object')
|
||||
? collect (value as Record<string, unknown>, path)
|
||||
: [path]
|
||||
})
|
||||
const baseline = i18n.getResourceBundle ('ja', namespace) ?? { }
|
||||
return collect (baseline).filter (key => {
|
||||
const value = i18n.getResource (locale, namespace, key)
|
||||
return typeof value !== 'string' || value.length === 0
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
{
|
||||
const missing = untranslatedKeys ('ain-Kana')
|
||||
if (missing.length > 0)
|
||||
console.warn ('[i18n] ain-Kana: untranslated navigation keys', missing)
|
||||
}
|
||||
+12
-31
@@ -1,38 +1,19 @@
|
||||
import { i18n } from '@/lib/i18n'
|
||||
|
||||
import type navigation from '@/locales/ja/navigation.json'
|
||||
import type { LocaleCode } from '@/lib/locales'
|
||||
|
||||
const japanese = {
|
||||
square: '広場',
|
||||
list: '一覧',
|
||||
search: '検索',
|
||||
add: '追加',
|
||||
history: '履歴',
|
||||
allHistory: '全体履歴',
|
||||
help: 'ヘルプ',
|
||||
tags: 'タグ',
|
||||
master: 'マスタ',
|
||||
nicoLinks: 'ニコニコ連携',
|
||||
materials: '素材',
|
||||
suppressions: '抑止',
|
||||
wiki: 'Wiki',
|
||||
create: '新規',
|
||||
edit: '編輯',
|
||||
entertainment: 'おたのしみ',
|
||||
theatre: '上映会 (β)',
|
||||
gekanator: 'グカネータ (β)',
|
||||
users: 'ユーザ',
|
||||
currentUser: 'お前',
|
||||
settings: '設定',
|
||||
legal: '法規',
|
||||
terms: '利用規約' }
|
||||
|
||||
export type UIMessageKey = keyof typeof japanese
|
||||
|
||||
const ainu: Partial<Record<UIMessageKey, string>> = { }
|
||||
export type UIMessageKey = keyof typeof navigation
|
||||
|
||||
|
||||
export const uiMessage = (locale: LocaleCode, key: UIMessageKey): string =>
|
||||
(locale === 'ain-Kana' ? ainu[key] : null) ?? japanese[key]
|
||||
export const uiMessage = (
|
||||
locale: LocaleCode,
|
||||
key: UIMessageKey,
|
||||
values: Record<string, string | number> = { },
|
||||
): string => i18n.t (key, { ...values, lng: locale, ns: 'navigation' })
|
||||
|
||||
|
||||
export const uiMessageLanguage = (locale: LocaleCode, key: UIMessageKey): 'ja' | 'ain' =>
|
||||
(locale === 'ain-Kana' && ainu[key] != null) ? 'ain' : 'ja'
|
||||
i18n.t (key, { lng: locale, ns: 'navigation', returnDetails: true }).usedLng === 'ain-Kana'
|
||||
? 'ain'
|
||||
: 'ja'
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { matchPath } from 'react-router-dom'
|
||||
|
||||
import { unlocalisedPath } from '@/lib/locales'
|
||||
|
||||
import type { LocaleCode } from '@/lib/locales'
|
||||
|
||||
const PAGE_LOCALES: { path: string; locale: LocaleCode }[] = [
|
||||
{ path: '/gekanator', locale: 'ja' }]
|
||||
|
||||
|
||||
export const requiredPageLocale = (pathname: string): LocaleCode | null =>
|
||||
PAGE_LOCALES.find (page =>
|
||||
matchPath ({ path: page.path, end: true }, unlocalisedPath (pathname)))?.locale ?? null
|
||||
@@ -0,0 +1,25 @@
|
||||
{ "posts": "ミンタㇻ",
|
||||
"postsWithCount": "ミンタㇻ({{count}})",
|
||||
"list": "オピッタ",
|
||||
"search": "イフナラ",
|
||||
"add": "アオ",
|
||||
"history": "フㇱコアンペ",
|
||||
"allHistory": "フㇱコアンペ オピッタ",
|
||||
"help": "ヤイカオパㇱ",
|
||||
"tags": "タㇰ",
|
||||
"master": "タㇰ オピッタ",
|
||||
"nicoLinks": "ニコニコ ウコテㇷ゚",
|
||||
"materials": "チカㇻペ",
|
||||
"suppressions": "アエアイカㇷ゚テㇷ゚",
|
||||
"wiki": "Wiki",
|
||||
"create": "アシㇼ",
|
||||
"edit": "イヌイェㇷ゚",
|
||||
"entertainment": "シノッ",
|
||||
"theatre": "チモイェノカ ヌカㇻ ウウェカㇻ(β)",
|
||||
"gekanator": "Gekanator(β)",
|
||||
"users": "イイェイワンケクㇽ",
|
||||
"currentUser": "エアニ",
|
||||
"settings": "イアヌ",
|
||||
"legal": "イレンカ",
|
||||
"terms": "エイワンケ イレンカ",
|
||||
"more": "モㇱマ アン ペ »" }
|
||||
@@ -0,0 +1,25 @@
|
||||
{ "posts": "広場",
|
||||
"postsWithCount": "広場 ({{count}})",
|
||||
"list": "一覧",
|
||||
"search": "検索",
|
||||
"add": "追加",
|
||||
"history": "履歴",
|
||||
"allHistory": "全体履歴",
|
||||
"help": "ヘルプ",
|
||||
"tags": "タグ",
|
||||
"master": "マスタ",
|
||||
"nicoLinks": "ニコニコ連携",
|
||||
"materials": "素材",
|
||||
"suppressions": "抑止",
|
||||
"wiki": "Wiki",
|
||||
"create": "新規",
|
||||
"edit": "編輯",
|
||||
"entertainment": "おたのしみ",
|
||||
"theatre": "上映会 (β)",
|
||||
"gekanator": "グカネータ (β)",
|
||||
"users": "ユーザ",
|
||||
"currentUser": "お前",
|
||||
"settings": "設定",
|
||||
"legal": "法規",
|
||||
"terms": "利用規約",
|
||||
"more": "その他 »" }
|
||||
+5
-1
@@ -1,9 +1,11 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { HelmetProvider } from 'react-helmet-async'
|
||||
import { I18nextProvider } from 'react-i18next'
|
||||
|
||||
import '@/index.css'
|
||||
import App from '@/App'
|
||||
import { i18n } from '@/lib/i18n'
|
||||
import { applyClientAnimationMode, getClientAnimationMode } from '@/lib/settings'
|
||||
|
||||
const helmetContext = { }
|
||||
@@ -19,6 +21,8 @@ applyClientAnimationMode (getClientAnimationMode ())
|
||||
createRoot (document.getElementById ('root')!).render (
|
||||
<HelmetProvider context={helmetContext}>
|
||||
<QueryClientProvider client={client}>
|
||||
<App/>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<App/>
|
||||
</I18nextProvider>
|
||||
</QueryClientProvider>
|
||||
</HelmetProvider>)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useMemo } from 'react'
|
||||
import { createElement, useMemo } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import { localiseUIChildren } from '@/components/LocalisedText'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { MDXComponents } from 'mdx/types'
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { ComponentProps, HTMLAttributes } from 'react'
|
||||
|
||||
|
||||
const MDXLink = (
|
||||
@@ -50,3 +52,28 @@ const MDXLink = (
|
||||
|
||||
|
||||
export const useMDXComponents = (): MDXComponents => ({ a: MDXLink })
|
||||
|
||||
|
||||
const ainuElement = (element: string) => {
|
||||
const AinuElement = ({ children, className, ...props }: HTMLAttributes<HTMLElement>) =>
|
||||
createElement (element,
|
||||
{ ...props, lang: 'ain-Kana', className: cn ('ainu-ui-text', className) },
|
||||
localiseUIChildren (children, 'ain-Kana'))
|
||||
return AinuElement
|
||||
}
|
||||
|
||||
|
||||
// Opt in only for authored Ainu UI documents, never user-supplied Markdown.
|
||||
export const ainuMDXComponents: MDXComponents = {
|
||||
p: ainuElement ('p'),
|
||||
h1: ainuElement ('h1'),
|
||||
h2: ainuElement ('h2'),
|
||||
h3: ainuElement ('h3'),
|
||||
h4: ainuElement ('h4'),
|
||||
h5: ainuElement ('h5'),
|
||||
h6: ainuElement ('h6'),
|
||||
li: ainuElement ('li'),
|
||||
dt: ainuElement ('dt'),
|
||||
dd: ainuElement ('dd'),
|
||||
th: ainuElement ('th'),
|
||||
td: ainuElement ('td') }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
|
||||
import PrefetchLink from '@/components/PrefetchLink'
|
||||
import LocalisedText from '@/components/LocalisedText'
|
||||
import { menuOutline } from '@/components/TopNav'
|
||||
import SectionTitle from '@/components/common/SectionTitle'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
@@ -25,7 +26,11 @@ const MorePage: FC = () => {
|
||||
<div key={i} className="flex-1 mx-16">
|
||||
{menu.slice (4 * i, 4 * (i + 1)).map ((item, j) => (
|
||||
<section key={j}>
|
||||
<SectionTitle className="font-bold">{item.name}</SectionTitle>
|
||||
<SectionTitle className="font-bold">
|
||||
<LocalisedText language={item.nameLanguage ?? 'ja'}>
|
||||
{item.name}
|
||||
</LocalisedText>
|
||||
</SectionTitle>
|
||||
<ul>
|
||||
{item.subMenu
|
||||
.filter (subItem => (subItem.visible ?? true))
|
||||
@@ -36,7 +41,9 @@ const MorePage: FC = () => {
|
||||
target={subItem.to.slice (0, 2) === '//'
|
||||
? '_blank'
|
||||
: undefined}>
|
||||
{subItem.name}
|
||||
<LocalisedText language={subItem.nameLanguage ?? 'ja'}>
|
||||
{subItem.name}
|
||||
</LocalisedText>
|
||||
</PrefetchLink>
|
||||
</li>)))}
|
||||
</ul>
|
||||
@@ -45,4 +52,4 @@ const MorePage: FC = () => {
|
||||
</MainArea>)
|
||||
}
|
||||
|
||||
export default MorePage
|
||||
export default MorePage
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export const translationStatus = 'untranslated-draft'
|
||||
|
||||
export const reviewRequired = [
|
||||
'前文:利用による同意と適用範囲',
|
||||
'第1条:サービスの位置づけ・仕様変更',
|
||||
'第2条:公開方針・利用者区分・権限',
|
||||
'第3条:認証情報・引継ぎコード・責任',
|
||||
'第4条:編輯義務・承認・可撤回の権限',
|
||||
'第5条:各禁止行為と権利侵害の範囲',
|
||||
'第6条:利用者の責任・非独占的利用許諾・権利帰属',
|
||||
'第7条:外部サービスの条件・保証の否定',
|
||||
'第8条:履歴保持・差戻・削除・完全消去の例外',
|
||||
'第9条:利用制限・資格取消・BAN・開示義務',
|
||||
'第10条:未成年の安全と表示制限',
|
||||
'第11条:聯絡方法・回答義務と保証の否定',
|
||||
'第12条:免責・保証の否定・サービス停止',
|
||||
'第13条:規約変更・発効時点・継続利用による同意',
|
||||
'第14条:日本法・専属的合意管轄・法令上の例外',
|
||||
'附則:適用日']
|
||||
|
||||
{/* No Ainu translation has been supplied or approved. Do not publish this
|
||||
draft or substitute a phonetic transcription of the Japanese source. */}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import MainArea from '@/components/layout/MainArea'
|
||||
import { SITE_TITLE } from '@/config'
|
||||
import { dateString } from '@/lib/utils'
|
||||
|
||||
export const lastUpdatedAt = dateString ('2026-04-12', 'hour')
|
||||
|
||||
|
||||
<MainArea>
|
||||
<Helmet>
|
||||
<title>{`利用規約 | ${ SITE_TITLE }`}</title>
|
||||
</Helmet>
|
||||
|
||||
<article lang="ja" className="prose dark:prose-invert mx-auto p-4">
|
||||
# 利用規約
|
||||
|
||||
最終更新日: {lastUpdatedAt}
|
||||
|
||||
この利用規約(以下「本規約」)は、ぼざクリ タグ広場(以下「本サービス」)の利用条件を定めるものです。利用者は、本サービスを利用した時点で、本規約に同意したものとみなされます。
|
||||
|
||||
## 第 1 条 本サービスの位置づけ
|
||||
|
||||
1. 本サービスは、タグ・Wiki・外部リンクの整理を中心とする知識共有基盤です。
|
||||
2. 本サービスの中心価値は、コンテンツそのものの再配布ではなく、タグを軸にした整理、検索、再発見、および周辺知識の蓄積にあります。
|
||||
3. 本サービスは、運営上の必要に応じて、機能、公開範囲、名称、URL、表示内容その他の仕様を変更することがあります。
|
||||
|
||||
## 第 2 条 公開方針と利用者区分
|
||||
|
||||
1. 本サービスは、初回一般公開時点では、**誰でも閲覧できる一方で、投稿・編輯は申請制** とします。
|
||||
2. 初回一般公開時点では、通常の農奴は閲覧のみを行えます。
|
||||
3. 投稿、タグ編輯、Wiki 編輯その他の耕作行為は、運営が承認した利用者(以下「耕作員」)に限って認めます。
|
||||
4. 独裁者は、耕作員に加えて、差戻、削除、利用制限、その他の管理操作を行えます。
|
||||
5. 運営は、履歴管理、差戻、BAN 運用、監査導線その他の運営装備がじゅうぶんに整ったと判断した場合、農奴に一部の編輯権限を開放することがあります。
|
||||
6. 利用者区分、権限範囲、申請条件、承認基準、承認後の取扱いは、運営が必要に応じて定め、変更できます。
|
||||
|
||||
## 第 3 条 利用開始と引継ぎコード
|
||||
|
||||
1. 本サービスでは、一般的な Id. / パスワード方式ではなく、運営が別途定める認証情報または引継ぎコードを用いる場合があります。
|
||||
2. 利用者は、自身に割り当てられた引継ぎコード、認証情報、端末上の保存情報を自己の責任で管理するものとします。
|
||||
3. 利用者は、自己の引継ぎコードまたは認証情報を第 3 者に譲渡、貸与、共有、漏洩してはなりません。
|
||||
4. 引継ぎコードの漏洩、第 3 者利用、紛失、盗用その他の事故によって利用者または第 3 者に生じた損害について、運営は責任を負いません。
|
||||
5. 運営は、本人確認、濫用対策、監査対応または保守のため、利用情報とアクセス元情報を関聯づけて扱うことがあります。
|
||||
|
||||
## 第 4 条 申請制編輯の基本ルール
|
||||
|
||||
1. 耕作員は、タグ整理基盤の品質維持を最優先し、個人的な所有主張ではなく、検索性、再利用性、可読性、整合性を重視して編輯しなければなりません。
|
||||
2. 耕作員は、主観的な好悪、内輪ネタ、報復、私怨、対立誘導のためにタグや Wiki を操作してはなりません。
|
||||
3. 耕作員は、誤りの修正、体系の整理、リンクの保守、知識の補足を目的として編輯を行うものとします。
|
||||
4. 運営は、申請内容、過去の行動、編輯品質、聯絡可能性、運営負荷その他の事情を考慮して、承認、保留、拒否、取消を行えます。
|
||||
5. 耕作員資格は権利ではなく、運営が本サービスの維持のために付与する可撤回の権限です。
|
||||
|
||||
## 第 5 条 禁止事項
|
||||
|
||||
利用者は、以下の行為をしてはなりません。
|
||||
|
||||
1. 法令または公序良俗に違反する行為。
|
||||
2. 犯罪を助長し、またはこれに結びつく行為。
|
||||
3. 著作権、著作者人格権、商標権、肖像権、パブリシティ権、プライバシー権その他第 3 者の権利を侵害する行為。
|
||||
4. 無断転載、違法アップロード、違法複製物、海賊版、権限のない転載先への誘導、またはそれらを正当化、拡散、補助する行為。
|
||||
5. 実在人物に関する名誉毀損、侮辱、差別、脅迫、晒し、つきまとい、嫌がらせ、私刑の扇動その他の加害行為。
|
||||
6. 個人情報、非公開情報、秘匿されるべき情報を本人の承諾なく掲載、送信、共有、推測可能な形で開示する行為。
|
||||
7. 虚偽の情報、誤解を招く情報、出典を偽装した情報、意図的なミスリード、荒らし目的のタグづけ、関係のないタグの大量付与、分類妨碍、検索妨碍その他の品質破壊行為。
|
||||
8. マルウェア、フィッシング、詐欺、誘導広告、悪質なリダイレクト、危険な外部リンクその他利用者または運営に危害を与える行為。
|
||||
9. 本サービスの趣旨に照らして不相当な政治的扇動、宗教勧誘、商業宣伝、連鎖的勧誘、スパム、同一内容の反復送信。
|
||||
10. 未成年の安全に反する行為、児童性的搾取、違法または著しく不適切な性的表現、過度に露骨な性表現や残虐表現を、一般公開導線に無警告で流し込む行為。
|
||||
11. 運営、他の利用者、外部サービスまたは第 3 者に著しい負担、不利益、混乱を生じさせる行為。
|
||||
12. 前各号のいずれかを試みる行為、教唆する行為、容易にする行為。
|
||||
13. その他、運営が本サービスの目的または安全な運営に照らして不適切と判断する行為。
|
||||
|
||||
## 第 6 条 投稿、タグ、Wiki 等の取扱い
|
||||
|
||||
1. 利用者は、自らが投稿、編輯、登録、送信または変更する情報について、必要な権利を有し、または適法に利用できる状態でなければなりません。
|
||||
2. 利用者は、自らが行った投稿、タグづけ、Wiki 編輯、説明文、コメント、関聯づけその他の行為について責任を負います。
|
||||
3. 利用者は、運営に対し、本サービスの運営、表示、複製、保存、配信、整形、引用、履歴表示、差戻、バックアップ、障碍対応および弘報のために必要な範囲で、当該利用者生成情報を無償で利用する非独占的な権利を許諾するものとします。
|
||||
4. 前項の許諾は、本サービスの運営上必要な範囲に限られ、利用者の権利帰属自体を運営へ移転するものではありません。
|
||||
5. 運営は、分類整合性、表記統一、誤記修正、別名統合、差戻その他の理由により、投稿、タグ、Wiki その他の内容を編輯、非表示化、削除、統合、分割または凍結できます。
|
||||
|
||||
## 第 7 条 外部リンクと埋め込み
|
||||
|
||||
1. 本サービスは、外部サイトへのリンク、外部コンテンツの埋め込みまたはそれらに関するメタデータを表示する場合があります。
|
||||
2. 外部リンク先または埋め込み先の権利、利用条件、公開範囲、削除方針、広告、追跡、Cookie その他の取扱いは、当該外部サービスの定めに従います。
|
||||
3. 運営は、外部リンク先の適法性、安全性、継続性、正確性、品質、可用性、または内容の完全性を保証しません。
|
||||
4. 外部権利者からの申立て、運営判断、法令対応または安全性確保のため、運営は外部リンク、埋め込み、サムネイル、説明文その他の表示を制限、差替え、非表示または削除できます。
|
||||
|
||||
## 第 8 条 履歴、差戻、削除
|
||||
|
||||
1. 本サービスでは、保守、監査、荒らし対策、説明責任その他の目的で、投稿、タグ、Wiki その他の変更履歴を保持し、表示し、または内部的に参照することがあります。
|
||||
2. 利用者は、一度行った編輯が、後に差戻、修正、非表示化または削除されることがあることをあらかじめ承諾するものとします。
|
||||
3. 利用者が削除を希望した場合でも、法令上、保守上、監査上、紛争対応上またはバックアップ上の必要により、直ちに完全消去できないことがあります。
|
||||
4. 運営は、本サービス全体の健全性を維持するため、説明の有無を問わず、履歴の表示範囲、保存期間、差戻方針、削除方針を定め、変更できます。
|
||||
|
||||
## 第 9 条 利用制限、資格取消、BAN
|
||||
|
||||
1. 運営は、利用者が次のいずれかに該当すると判断した場合、事前の通知なく、または通知後に、投稿・編輯の制限、耕作員資格の取消、コンテンツの非表示または削除、引継ぎコードの失効、ユーザ BAN、IP BAN その他必要な措置を行えます。
|
||||
- 本規約に違反した場合
|
||||
- 本サービスの趣旨に反する運用妨碍、荒らし、品質破壊行為を行った場合
|
||||
- 運営からの確認、修正要請、停止要請に合理的理由なく応じない場合
|
||||
- 登録情報、申請内容または説明に虚偽がある場合
|
||||
- 安全性、法令順守、運営継続の観点から措置が必要と判断された場合
|
||||
2. 運営は、前項の措置について、その理由、基準、証拠または内部判断過程を常に開示する義務を負いません。
|
||||
3. 利用制限または資格取消後も、運営は、必要に応じて履歴、ログ、申請記録、通報記録その他のデータを保持できます。
|
||||
|
||||
## 第 10 条 未成年の利用
|
||||
|
||||
1. 運営は、未成年の安全確保の観点から、年齢に応じた表示制限、導線制御、非表示化、削除、申請拒否その他の措置を行えます。
|
||||
2. 利用者は、未成年が閲覧しうる一般公開面において、未成年に不適切な内容を無警告で流し込まないものとします。
|
||||
|
||||
## 第 11 条 お問い合わせ、通報、御意見番
|
||||
|
||||
1. 利用者は、本サービスが別途案内する問い合わせ、通報または御意見板の導線を通じて、バグ報告、問題報告、削除要請その他の聯絡を行えます。
|
||||
2. 運営は、すべての問い合わせに回答する義務を負わず、回答期限、対応結果または対応方法を保証しません。
|
||||
|
||||
## 第 12 条 免責
|
||||
|
||||
1. 運営は、本サービスについて、特定目的適合性、完全性、正確性、継続性、安全性、無瑕疵性、または利用者の期待への適合を保証しません。
|
||||
2. 運営は、外部リンク先、外部埋め込み先、第 3 者投稿、利用者同士の紛争、通信障碍、データ消失、誤分類、誤リンク、誤記、差戻、機能停止または仕様変更によって生じた損害について、責任を負いません。
|
||||
3. 本サービスは、予告なく停止、終了、変更または縮小されることがあります。
|
||||
|
||||
## 第 13 条 規約の変更
|
||||
|
||||
1. 運営は、法令改正、機能追加、運用方針の変更、安全対策、表現調整その他の理由により、本規約を変更できます。
|
||||
2. 変更後の本規約は、本サービス上に掲載された時点または運営が別途定める時点から効力を生じます。
|
||||
3. 変更後に利用を継続した利用者は、変更後の本規約に同意したものとみなされます。
|
||||
|
||||
## 第 14 条 準拠法および管轄
|
||||
|
||||
1. 本規約および本サービスの利用には、日本法を準拠法とします。
|
||||
2. 本規約または本サービスに関して生じた一切の紛争については、運営の所在地を管轄する裁判所を第 1 審の専属的合意管轄裁判所とします。ただし、法令に別段の定めがある場合はこの限りではありません。
|
||||
|
||||
## 附則
|
||||
|
||||
本規約は、{lastUpdatedAt} から適用します。
|
||||
</article>
|
||||
</MainArea>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ainuMDXComponents } from '@/mdx-components'
|
||||
import { useLocale } from '@/lib/useLocale'
|
||||
import AinuTerms from '@/pages/TOSPage.ain-Kana.mdx'
|
||||
import NipponeseTerms from '@/pages/TOSPage.ja.mdx'
|
||||
|
||||
// Change only after the user has reviewed the translation and approved publication.
|
||||
const AINU_TERMS_PUBLISHED = false
|
||||
|
||||
|
||||
const TOSPage = () => {
|
||||
const locale = useLocale ()
|
||||
return (locale === 'ain-Kana' && AINU_TERMS_PUBLISHED)
|
||||
? <AinuTerms components={ainuMDXComponents}/>
|
||||
: <NipponeseTerms/>
|
||||
}
|
||||
|
||||
|
||||
export default TOSPage
|
||||
@@ -229,6 +229,7 @@ export type Menu = MenuItem[]
|
||||
|
||||
export type MenuInvisibleItem = {
|
||||
name: ReactNode
|
||||
nameLanguage?: 'ja' | 'ain'
|
||||
to?: string
|
||||
base?: string
|
||||
visible: false
|
||||
@@ -238,6 +239,7 @@ export type MenuItem = MenuVisibleItem | MenuInvisibleItem
|
||||
|
||||
export type MenuVisibleItem = {
|
||||
name: ReactNode
|
||||
nameLanguage?: 'ja' | 'ain'
|
||||
to: string
|
||||
base?: string
|
||||
visible?: true
|
||||
@@ -340,6 +342,7 @@ export type SubMenuItem = SubMenuComponentItem | SubMenuStringItem
|
||||
|
||||
export type SubMenuStringItem = {
|
||||
name: ReactNode
|
||||
nameLanguage?: 'ja' | 'ain'
|
||||
to: string
|
||||
visible?: boolean }
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする