アイヌ語追加 (#428) (#433)

Reviewed-on: #433
Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #433 でマージされました。
このコミットが含まれているのは:
2026-10-20 20:44:14 +09:00
committed by みてるぞ
コミット 084a090cab
294個のファイルの変更、13978行の追加、3461行の削除
+12 -8
ファイルの表示
@@ -1,8 +1,10 @@
import { act, render, screen, waitFor } from '@testing-library/react'
import { act, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import PostImportRowForm from '@/components/posts/import/PostImportRowForm'
import { buildPostImportRow } from '@/test/postImportFactories'
import { renderWithProviders as render } from '@/test/render'
import { textContentOf } from '@/test/text'
import type { DialogueFormAction, DialogueFormControls } from '@/lib/dialogues/useDialogue'
@@ -37,7 +39,7 @@ describe ('PostImportRowForm', () => {
const titleInput = screen.getByDisplayValue ('manual title')
await waitFor (() => expect (actions.at (-1)?.length).toBe (2))
const reset = actions.at (-1)?.find (action => action.label === '変更をリセット')
const reset = actions.at (-1)?.find (action => textContentOf (action.label) === '変更をリセット')
expect (reset).toMatchObject ({ placement: 'start', variant: 'danger', disabled: false })
await act (async () => {
await reset?.onSelect ()
@@ -47,7 +49,7 @@ describe ('PostImportRowForm', () => {
expect (titleInput).toHaveValue ('')
expect (onSave).not.toHaveBeenCalled ()
const save = actions.at (-1)?.find (action => action.label === '編輯内容を保存')
const save = actions.at (-1)?.find (action => textContentOf (action.label) === '編輯内容を保存')
await act (async () => {
await save?.onSelect ()
})
@@ -76,7 +78,7 @@ describe ('PostImportRowForm', () => {
await waitFor (() => expect (actions.length).toBe (2))
await act (async () => {
await actions.find (action => action.label === '変更をリセット')?.onSelect ()
await actions.find (action => textContentOf (action.label) === '変更をリセット')?.onSelect ()
})
expect (screen.getByDisplayValue ('manual title')).toBeInTheDocument ()
@@ -124,7 +126,7 @@ describe ('PostImportRowForm', () => {
await waitFor (() => expect (actions.length).toBe (2))
await act (async () => {
void actions.find (action => action.label === '編輯内容を保存')?.onSelect ()
void actions.find (action => textContentOf (action.label) === '編輯内容を保存')?.onSelect ()
})
expect (onSave).toHaveBeenCalledWith ({
@@ -171,7 +173,7 @@ describe ('PostImportRowForm', () => {
expect (screen.getByDisplayValue ('tag1')).toBeInTheDocument ()
await act (async () => {
await actions.find (action => action.label === '編輯内容を保存')?.onSelect ()
await actions.find (action => textContentOf (action.label) === '編輯内容を保存')?.onSelect ()
})
expect (onSave).toHaveBeenCalledWith ({
@@ -218,7 +220,8 @@ describe ('PostImportRowForm', () => {
onSave={vi.fn ()}/>)
await waitFor (() => expect (actions.length).toBe (2))
expect (actions.find (action => action.label === '変更をリセット')?.disabled).toBe (false)
const reset = actions.find (action => textContentOf (action.label) === '変更をリセット')
expect (reset?.disabled).toBe (false)
})
it (
@@ -249,7 +252,8 @@ describe ('PostImportRowForm', () => {
let savePromise: Promise<boolean | void> | undefined
await act (async () => {
savePromise = actions.find (action => action.label === '編輯内容を保存')?.onSelect ()
const save = actions.find (action => textContentOf (action.label) === '編輯内容を保存')
savePromise = save?.onSelect ()
})
await waitFor (() => {
+24 -19
ファイルの表示
@@ -7,8 +7,11 @@ import PostDurationField from '@/components/posts/PostDurationField'
import PostTextField from '@/components/posts/PostTextField'
import PostImportThumbnailPreview from '@/components/posts/import/PostImportThumbnailPreview'
import { hasThumbnailBaseValue, hasVideoTag } from '@/lib/postImportRows'
import { renderPostImportMessage } from '@/components/posts/import/postImportRowStatus'
import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import type { FC } from 'react'
import type { FC, ReactNode } from 'react'
import type { DialogueFormControls } from '@/lib/dialogues/useDialogue'
import type { PostImportEditableDraft, PostImportRow } from '@/lib/postImportTypes'
@@ -47,9 +50,6 @@ const buildResetDraft = (row: PostImportRow): Draft => ({
duration: String (row.resetSnapshot.attributes.duration ?? ''),
thumbnailFile: undefined })
const groupedMessages = (...values: (string[] | undefined)[]): string[] =>
[...new Set (values.flatMap (value => value ?? []))]
const sameDraft = (left: Draft, right: Draft): boolean =>
left.url === right.url
&& left.title === right.title
@@ -98,6 +98,11 @@ const PostImportRowForm: FC<Props> = (
controls,
onSave },
) => {
const locale = useLocale ()
const { label } = useUITranslation ('imports')
const groupedMessages = (...values: (string[] | undefined)[]): ReactNode[] =>
[...new Set (values.flatMap (value => value ?? []))]
.map (value => renderPostImportMessage (value, locale))
const [draft, setDraft] = useState<Draft> (() => buildDraft (row))
const [messageRow, setMessageRow] = useState<PostImportRow | null> (null)
const [saving, setSaving] = useState (false)
@@ -144,9 +149,9 @@ const PostImportRowForm: FC<Props> = (
return false
const confirmed = await controls.confirm ({
title: '変更をリセットしますか?',
confirmText: 'リセット',
cancelText: '取消',
title: label ('shared:messages.resetChanges'),
confirmText: label ('shared:actions.reset'),
cancelText: label ('shared:actions.cancel'),
variant: 'danger' })
if (!(confirmed))
return false
@@ -156,7 +161,7 @@ const PostImportRowForm: FC<Props> = (
setMessageRow (null)
setCommittedThumbnailBase (resetDraft.thumbnailBase)
return false
}, [controls, resetDisabled, resetDraft])
}, [controls, resetDisabled, resetDraft, label])
const save = useCallback (async (): Promise<boolean> => {
setSaving (true)
@@ -178,16 +183,16 @@ const PostImportRowForm: FC<Props> = (
useEffect (() => {
controls.setActions ([{
label: '変更をリセット',
label: label ('review.reset'),
placement: 'start',
variant: 'danger',
disabled: resetDisabled,
onSelect: reset },
{
label: '編輯内容を保存',
label: label ('review.save'),
disabled: saving,
onSelect: save }])
}, [controls, resetDisabled, reset, save, saving])
}, [controls, resetDisabled, reset, save, saving, label])
return (
<>
@@ -210,17 +215,17 @@ const PostImportRowForm: FC<Props> = (
value: draft.url,
onChange: value => update ('url', value),
disabled: saving,
warnings: displayRow.fieldWarnings.url,
warnings: groupedMessages (displayRow.fieldWarnings.url),
errors: groupedMessages (
displayRow.validationErrors.url,
displayRow.importErrors?.url) }}
thumbnailField={
<>
<PostTextField
label="サムネール"
label={label ('shared:labels.thumbnail')}
value={draft.thumbnailBase}
disabled={saving}
warnings={currentThumbnailWarnings}
warnings={groupedMessages (currentThumbnailWarnings)}
errors={groupedMessages (
displayRow.validationErrors.thumbnailBase,
displayRow.importErrors?.thumbnailBase)}
@@ -244,7 +249,7 @@ const PostImportRowForm: FC<Props> = (
value: draft.title,
onChange: value => update ('title', value),
disabled: saving,
warnings: displayRow.fieldWarnings.title,
warnings: groupedMessages (displayRow.fieldWarnings.title),
errors: groupedMessages (
displayRow.validationErrors.title,
displayRow.importErrors?.title) },
@@ -270,7 +275,7 @@ const PostImportRowForm: FC<Props> = (
value: draft.tags,
onChange: value => update ('tags', value),
disabled: saving,
warnings: displayRow.fieldWarnings.tags,
warnings: groupedMessages (displayRow.fieldWarnings.tags),
errors: groupedMessages (
displayRow.validationErrors.tags,
displayRow.importErrors?.tags),
@@ -293,9 +298,9 @@ const PostImportRowForm: FC<Props> = (
displayRow.validationErrors.videoMs,
displayRow.importErrors?.videoMs)}/>)
: null}/>
<FieldWarning messages={displayRow.baseWarnings}/>
<FieldError messages={displayRow.validationErrors.base}/>
<FieldError messages={displayRow.importErrors?.base}/>
<FieldWarning messages={groupedMessages (displayRow.baseWarnings)}/>
<FieldError messages={groupedMessages (displayRow.validationErrors.base)}/>
<FieldError messages={groupedMessages (displayRow.importErrors?.base)}/>
</div>
</div>
</div>
+25 -16
ファイルの表示
@@ -3,7 +3,11 @@ import PostImportTagLinks from '@/components/posts/import/PostImportTagLinks'
import { Button } from '@/components/ui/button'
import PostImportThumbnailPreview from '@/components/posts/import/PostImportThumbnailPreview'
import PostImportStatusBadge from '@/components/posts/import/PostImportStatusBadge'
import { displayPostImportStatus } from '@/components/posts/import/postImportRowStatus'
import { displayPostImportStatus,
renderPostImportMessage } from '@/components/posts/import/postImportRowStatus'
import LocalisedText from '@/components/LocalisedText'
import { useLocale } from '@/lib/useLocale'
import { useUITranslation } from '@/lib/useUITranslation'
import {
canEditReviewRow,
canRetryResultRow,
@@ -14,6 +18,7 @@ import { cn, originalCreatedAtString } from '@/lib/utils'
import type { FC } from 'react'
import type { PostImportRow } from '@/lib/postImportTypes'
import type { LocaleCode } from '@/lib/locales'
type Props = {
row: PostImportRow
@@ -33,10 +38,10 @@ const summaryWarning = (row: PostImportRow): string | null =>
?? row.baseWarnings?.[0]
?? null
const summaryDate = (row: PostImportRow): string =>
const summaryDate = (row: PostImportRow, locale: LocaleCode): string =>
originalCreatedAtString (
row.attributes.originalCreatedFrom?.toString () ?? null,
row.attributes.originalCreatedBefore?.toString () ?? null)
row.attributes.originalCreatedBefore?.toString () ?? null, locale)
const PostImportRowSummary: FC<Props> = (
@@ -52,6 +57,10 @@ const PostImportRowSummary: FC<Props> = (
showActions = true,
showSkipToggle = false },
) => {
const locale = useLocale ()
const { label } = useUITranslation ('imports')
const localisedRowMessages = rowMessages?.map (value =>
renderPostImportMessage (value, locale))
const warning = summaryWarning (row)
const displayStatus = displayPostImportStatus (row)
const editVisible = onEdit != null
@@ -69,7 +78,7 @@ const PostImportRowSummary: FC<Props> = (
checked={skipChecked}
onChange={event => onToggleSkip?.(event.target.checked)}
disabled={skipDisabled === true}/>
<span>スキップ</span>
<span>{label ('shared:actions.skip')}</span>
</label>)
: null
@@ -96,17 +105,17 @@ const PostImportRowSummary: FC<Props> = (
</div>
<PostImportTagLinks tags={row.displayTags}/>
<div className="text-xs text-neutral-500 dark:text-neutral-400">
{summaryDate (row)}
<LocalisedText language={locale}>{summaryDate (row, locale)}</LocalisedText>
</div>
{showDuration && (
<div className="text-xs text-neutral-500 dark:text-neutral-400">
動画時間 {duration}
{label ('review.duration', { duration })}
</div>)}
{warning && (
<div className="text-xs text-amber-700 dark:text-amber-200">
{warning}
{renderPostImportMessage (warning, locale)}
</div>)}
<FieldError messages={rowMessages}/>
<FieldError messages={localisedRowMessages}/>
</div>
<div className="space-y-1">
{displayStatus != null && <PostImportStatusBadge value={displayStatus}/>}
@@ -120,7 +129,7 @@ const PostImportRowSummary: FC<Props> = (
variant="outline"
onClick={onEdit}
disabled={editDisabled === true || !(editAllowed)}>
編輯
{label ('shared:actions.edit')}
</Button>)}
{showActions && retryAllowed && (
<Button
@@ -128,7 +137,7 @@ const PostImportRowSummary: FC<Props> = (
variant="outline"
onClick={onRetry}
disabled={retryDisabled === true}>
再試行
{label ('shared:actions.retry')}
</Button>)}
</div>
</div>
@@ -156,17 +165,17 @@ const PostImportRowSummary: FC<Props> = (
</div>
<PostImportTagLinks tags={row.displayTags}/>
<div className="text-xs text-neutral-500 dark:text-neutral-400">
{summaryDate (row)}
<LocalisedText language={locale}>{summaryDate (row, locale)}</LocalisedText>
</div>
{showDuration && (
<div className="text-xs text-neutral-500 dark:text-neutral-400">
動画時間 {duration}
{label ('review.duration', { duration })}
</div>)}
{warning && (
<div className="text-xs text-amber-700 dark:text-amber-200">
{warning}
{renderPostImportMessage (warning, locale)}
</div>)}
<FieldError messages={rowMessages}/>
<FieldError messages={localisedRowMessages}/>
{skipControl}
</div>
</div>
@@ -179,7 +188,7 @@ const PostImportRowSummary: FC<Props> = (
variant="outline"
onClick={onEdit}
disabled={editDisabled === true || !(editAllowed)}>
編輯
{label ('shared:actions.edit')}
</Button>)}
{retryAllowed && (
<Button
@@ -188,7 +197,7 @@ const PostImportRowSummary: FC<Props> = (
variant="outline"
onClick={onRetry}
disabled={retryDisabled === true}>
再試行
{label ('shared:actions.retry')}
</Button>)}
</div>)}
</div>
+2 -9
ファイルの表示
@@ -1,4 +1,5 @@
import StatusBadge from '@/components/common/StatusBadge'
import TranslatedText from '@/components/TranslatedText'
import type { FC } from 'react'
@@ -8,14 +9,6 @@ import type { PostImportBadgeValue } from '@/components/posts/import/postImportR
type Props = {
value: PostImportBadgeValue }
const LABELS: Record<PostImportBadgeValue, string> = {
ready: '登録可能',
error: '登録不可',
warning: '警告',
skipped: 'スキップ',
created: '登録済み',
failed: '登録失敗' }
const TONES: Record<PostImportBadgeValue, StatusBadgeTone> = {
ready: 'success',
error: 'warning',
@@ -27,7 +20,7 @@ const TONES: Record<PostImportBadgeValue, StatusBadgeTone> = {
const PostImportStatusBadge: FC<Props> = ({ value }) => (
<StatusBadge tone={TONES[value]}>
{LABELS[value]}
<TranslatedText namespace="imports" messageKey={`status.${ value }`}/>
</StatusBadge>)
export default PostImportStatusBadge
+2 -1
ファイルの表示
@@ -1,7 +1,8 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { fireEvent, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import PostImportThumbnailPreview from '@/components/posts/import/PostImportThumbnailPreview'
import { renderWithProviders as render } from '@/test/render'
describe ('PostImportThumbnailPreview', () => {
beforeEach (() => {
+1 -1
ファイルの表示
@@ -12,7 +12,7 @@ type Props = {
const PostImportThumbnailPreview: FC<Props> = (
{ url,
file,
alt = 'サムネール',
alt,
className = 'h-16 w-16' },
) => (
<PostThumbnailPreview
+41
ファイルの表示
@@ -1,3 +1,10 @@
import { createElement } from 'react'
import LocalisedText from '@/components/LocalisedText'
import { i18n, translateUIMessage } from '@/lib/i18n'
import type { ReactNode } from 'react'
import type { PostImportRow } from '@/lib/postImportTypes'
export type PostImportDisplayStatus =
@@ -10,6 +17,40 @@ export type PostImportDisplayStatus =
export type PostImportBadgeValue = PostImportDisplayStatus
const IMPORT_MESSAGE_KEYS = [
'validation.duplicateUrl', 'validation.processingFailed',
'validation.tooMany', 'validation.urlTooLong', 'validation.invalidUrl',
'validation.protocol', 'validation.duplicateRow', 'storage.readFailed',
'storage.writeFailed', 'storage.clearFailed', 'source.urlRequired',
'shared:labels.thumbnailMissing']
// Keep stored and compared messages intact; translate only recognised UI messages at display time.
export const renderPostImportMessage = (value: string, locale: string): ReactNode => {
for (const reference of IMPORT_MESSAGE_KEYS)
{
const [namespace, key] =
reference.includes (':')
? reference.split (':')
: ['imports', reference]
const template: unknown = i18n.getResource ('ja', namespace, key)
if (typeof template !== 'string')
continue
const names = [...template.matchAll (/\{\{(\w+)\}\}/g)].map (match => match[1])
const pattern = template.split (/(\{\{\w+\}\})/).map (part =>
part.startsWith ('{{')
? '(\\d+)'
: part.replace (/[.*+?^${}()|[\]\\]/g, '\\$&')).join ('')
const match = value.match (new RegExp (`^${pattern}$`))
if (match == null)
continue
const values = Object.fromEntries (names.map ((name, index) => [name, match[index + 1]]))
const result = translateUIMessage (key, namespace, locale, values)
return createElement (LocalisedText, { language: result.language, children: result.text })
}
return value
}
const hasWarnings = (row: PostImportRow): boolean =>
Object.values (row.fieldWarnings ?? { }).some (messages => messages.length > 0)
|| row.baseWarnings.length > 0