diff --git a/app/learn/chinese/02/page.tsx b/app/learn/chinese/02/page.tsx
index b640852..64dccd0 100644
--- a/app/learn/chinese/02/page.tsx
+++ b/app/learn/chinese/02/page.tsx
@@ -1,20 +1,18 @@
"use client";
-import { PinyinPractice } from "@/components/pinyin/PinyinPractice";
-import { characters } from "./characters";
+import { TranslationPractice } from "@/components/translation/TranslationPractice";
-export const title = "漢字からピンイン";
+export const title = "翻訳";
-export default function ChinesePinyinPage() {
+export default function ChineseTranslationPage() {
return (
-
漢字からピンイン
+
翻訳
- 基本的な漢字を1字ずつ見て、ピンインを入力しましょう。四声は母音を入力した直後に
- ↑/↓キーで選べます。
+ 中国語⇔日本語の訳文を書き、AIに添削してもらいましょう。
);
diff --git a/app/learn/chinese/02/characters.ts b/app/learn/chinese/03/characters.ts
similarity index 100%
rename from app/learn/chinese/02/characters.ts
rename to app/learn/chinese/03/characters.ts
diff --git a/app/learn/chinese/03/page.tsx b/app/learn/chinese/03/page.tsx
new file mode 100644
index 0000000..b640852
--- /dev/null
+++ b/app/learn/chinese/03/page.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { PinyinPractice } from "@/components/pinyin/PinyinPractice";
+import { characters } from "./characters";
+
+export const title = "漢字からピンイン";
+
+export default function ChinesePinyinPage() {
+ return (
+
+
漢字からピンイン
+
+ 基本的な漢字を1字ずつ見て、ピンインを入力しましょう。四声は母音を入力した直後に
+ ↑/↓キーで選べます。
+
+
+
+ );
+}
diff --git a/app/learn/french/02/page.tsx b/app/learn/french/02/page.tsx
index 71d6e16..68e9c8f 100644
--- a/app/learn/french/02/page.tsx
+++ b/app/learn/french/02/page.tsx
@@ -1,18 +1,16 @@
"use client";
-import { ConjugationPractice } from "@/components/verbs/ConjugationPractice";
-import { frenchConjugationConfig } from "@/lib/conjugation/fr/config";
-import { verbs } from "./verbs";
+import { TranslationPractice } from "@/components/translation/TranslationPractice";
-export const title = "動詞の活用";
+export const title = "翻訳";
-export default function FrenchVerbConjugationPage() {
+export default function FrenchTranslationPage() {
return (
-
動詞の活用
-
動詞・時制・法を選んで、活用形を入力しながら覚えましょう。
+
翻訳
+
フランス語⇔日本語の訳文を書き、AIに添削してもらいましょう。
-
+
);
diff --git a/app/learn/french/03/page.mdx b/app/learn/french/03/page.mdx
deleted file mode 100644
index 8c8b82e..0000000
--- a/app/learn/french/03/page.mdx
+++ /dev/null
@@ -1,30 +0,0 @@
-export const title = "冠詞の一覧と縮約形";
-
-# 冠詞の一覧と縮約形
-
-_第1章 品詞と文型より_
-
-## 冠詞の種類
-
-| | | | 男性形 | 女性形 |
-| ------ | -------- | ------ | ---------- | ------------- |
-| 可算 | 不定冠詞 | 単数形 | un | une |
-| 可算 | 不定冠詞 | 複数形 | des | des |
-| 可算 | 定冠詞 | 単数形 | le (l') | la (l') |
-| 可算 | 定冠詞 | 複数形 | les | les |
-| 不可算 | 部分冠詞 | - | du (de l') | de la (de l') |
-| 不可算 | 定冠詞 | - | le (l') | la (l') |
-
-- 括弧内は直後の名詞が母音始まりの時に用いる。
-- 「du」は「de le」の短縮版であり、部分冠詞は「de+定冠詞」で作られると考えてよい。
-
-## 冠詞の縮約形
-
-- à+le → au、à+les → aux
-- de+le → du、de+les → des
-
-## 否定文における冠詞「de」
-
-否定文において、①不定冠詞や部分冠詞が、②直接目的語に付いている場合、冠詞が「de」に変化する。
-
-- J'ai un chien. → Je n'ai pas de chien.
diff --git a/app/learn/french/03/page.tsx b/app/learn/french/03/page.tsx
new file mode 100644
index 0000000..98578d6
--- /dev/null
+++ b/app/learn/french/03/page.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import { ConjugationPractice } from "@/components/verbs/ConjugationPractice";
+import { frenchConjugationConfig } from "@/lib/conjugation/fr/config";
+import { verbs } from "./verbs";
+
+export const title = "動詞の活用";
+
+export default function FrenchVerbConjugationPage() {
+ return (
+
+
動詞の活用
+
動詞・時制・法を選んで、活用形を入力しながら覚えましょう。
+
+
+
+
+ );
+}
diff --git a/app/learn/french/02/verbs.ts b/app/learn/french/03/verbs.ts
similarity index 100%
rename from app/learn/french/02/verbs.ts
rename to app/learn/french/03/verbs.ts
diff --git a/app/learn/french/04/page.mdx b/app/learn/french/04/page.mdx
deleted file mode 100644
index a3dfbb8..0000000
--- a/app/learn/french/04/page.mdx
+++ /dev/null
@@ -1,66 +0,0 @@
-export const title = "基本動詞の直説法現在活用";
-
-# 基本動詞の直説法現在活用
-
-_第1章 品詞と文型より_
-
-## être(be動詞)
-
-| 人称 | 活用形 | 人称 | 活用形 |
-| ---------- | ------ | ---------- | ------ |
-| 一人称単数 | suis | 一人称複数 | sommes |
-| 二人称単数 | es | 二人称複数 | êtes |
-| 三人称単数 | est | 三人称複数 | sont |
-
-## avoir(have)
-
-| 人称 | 活用形 | 人称 | 活用形 |
-| ---------- | ------ | ---------- | ------ |
-| 一人称単数 | ai | 一人称複数 | avons |
-| 二人称単数 | as | 二人称複数 | avez |
-| 三人称単数 | a | 三人称複数 | ont |
-
-## faire(make, do)
-
-| 人称 | 活用形 | 人称 | 活用形 |
-| ---------- | ------ | ---------- | ------- |
-| 一人称単数 | fais | 一人称複数 | faisons |
-| 二人称単数 | fais | 二人称複数 | faites |
-| 三人称単数 | fait | 三人称複数 | font |
-
-## aller(go)
-
-| 人称 | 活用形 | 人称 | 活用形 |
-| ---------- | ------ | ---------- | ------ |
-| 一人称単数 | vais | 一人称複数 | allons |
-| 二人称単数 | vas | 二人称複数 | allez |
-| 三人称単数 | va | 三人称複数 | vont |
-
-## venir(come)
-
-| 人称 | 活用形 | 人称 | 活用形 |
-| ---------- | ------ | ---------- | -------- |
-| 一人称単数 | viens | 一人称複数 | venons |
-| 二人称単数 | viens | 二人称複数 | venez |
-| 三人称単数 | vient | 三人称複数 | viennent |
-
-## -er動詞
-
-| 人称 | 語尾 | 人称 | 語尾 |
-| ---------- | ---- | ---------- | ---- |
-| 一人称単数 | -e | 一人称複数 | -ons |
-| 二人称単数 | -es | 二人称複数 | -ez |
-| 三人称単数 | -e | 三人称複数 | -ent |
-
-- -gerで終わる動詞(manger等)の一人称複数形は -geons
-- -cerで終わる動詞(commencer等)の一人称複数形は -çons
-
-## -ir動詞
-
-| 人称 | 語尾 | 人称 | 語尾 |
-| ---------- | ---- | ---------- | ------- |
-| 一人称単数 | -is | 一人称複数 | -issons |
-| 二人称単数 | -is | 二人称複数 | -issez |
-| 三人称単数 | -it | 三人称複数 | -issent |
-
-- 複数形の活用は「-iss+er動詞の活用形」の形になっている。
diff --git a/app/learn/french/05/page.mdx b/app/learn/french/05/page.mdx
deleted file mode 100644
index 3eef46d..0000000
--- a/app/learn/french/05/page.mdx
+++ /dev/null
@@ -1,64 +0,0 @@
-export const title = "基数と序数と概数";
-
-# 基数と序数と概数
-
-_第1章 品詞と文型より_
-
-## 基数(0〜19)
-
-| 数 | 綴り | 数 | 綴り |
-| --- | --------- | --- | -------- |
-| 0 | zéro | 10 | dix |
-| 1 | un(une) | 11 | onze |
-| 2 | deux | 12 | douze |
-| 3 | trois | 13 | treize |
-| 4 | quatre | 14 | quatorze |
-| 5 | cinq | 15 | quinze |
-| 6 | six | 16 | seize |
-| 7 | sept | 17 | dix-sept |
-| 8 | huit | 18 | dix-huit |
-| 9 | neuf | 19 | dix-neuf |
-
-## 基数(20の倍数)
-
-| 数 | 綴り | 数 | 綴り |
-| --- | ------------- | --- | --------- |
-| 20 | vingt | 30 | trente |
-| 40 | quarante | 50 | cinquante |
-| 60 | soixante | - | - |
-| 80 | quatre-vingts | - | - |
-
-- 21〜99は上記の組み合わせで表現する:「{"A"の数字と"B"の数字の和となる基数}={"B"の基数}-{"A"の基数}」
- - 22:vingt-deux(20+2)
- - 75:soixante-quinze(60+15)
-- "B"として80を組み合わせに用いる場合は「quatre-vingt」(sなし)になる。
-- 基数10〜19を組み合わせで使うのは70〜79及び90〜99を表すときのみ。
-- 一桁目が1を含むとき、間に「et」を挟んでハイフンでつなぐ。
- - 31:trente-et-un
- - 91:quatre-vingt-onze
-
-## 3桁以上の基数
-
-| 桁 | 数 | 綴り |
-| ------- | ----------------- | -------- |
-| 3桁 | 100 | cent |
-| 4桁以上 | 1.000 | mille |
-| 4桁以上 | 1.000.000 | million |
-| 4桁以上 | 1.000.000.000 | milliard |
-| 4桁以上 | 1.000.000.000.000 | billion |
-
-## 序数
-
-原則:基数の後ろに-ièmeをつける。
-
-- deux → deuxième
-- eで終わる数字はeを取ってièmeをつける(quatre → quatrième)
-- 「un(une)」は不規則で「premier(première)」
-- cinq は語尾にuを加えて「cinquième」
-- neuf は語尾のfがvになり「neuvième」
-
-## 概数
-
-基数の語尾に-aineを付けて作る。女性名詞として扱われるため、不定冠詞「une」や定冠詞「la」を伴うことがほとんど(1000の概数millierだけ男性名詞)。後ろに名詞が続く場合は前置詞「de」が必要。
-
-- vingt → vingtaine
diff --git a/app/learn/french/06/page.mdx b/app/learn/french/06/page.mdx
deleted file mode 100644
index 311db40..0000000
--- a/app/learn/french/06/page.mdx
+++ /dev/null
@@ -1,24 +0,0 @@
-export const title = "時刻表現";
-
-# 時刻表現
-
-_第1章 品詞と文型より_
-
-「〜時(〜分)」は「〜heures(〜)」で表現する。
-
-- Il est deux heures dix. (2時10分です。)
-
-## 定型表現
-
-- 「〜時15分」は「et quart」
-- 「〜時30分」は「et demi(e)」
-- 「〜時15分前」は「moins le quart」
-- これらは「〜heures」の後に伴って表現する。
- - Il est sept heures et demie. (7時半です。)
-
-## 秒までの表記
-
-時間帯を秒数まで細かく指定する場合、「et」を用いて秒数を続け、単位も記載する。表記は「heure → h、minute → min、seconde → s」とする。
-
-- Il est seize heures vingt et une minute et dix-sept secondes.
- = Il est 16 h 21 min 17 s.(16時21分17秒です。)
diff --git a/app/learn/french/07/page.mdx b/app/learn/french/07/page.mdx
deleted file mode 100644
index e2df8af..0000000
--- a/app/learn/french/07/page.mdx
+++ /dev/null
@@ -1,11 +0,0 @@
-export const title = "日程表現";
-
-# 日程表現
-
-_第1章 品詞と文型より_
-
-事細かに表す場合は「le+曜日+日+月+年」で表現する。日程を単に述べる場合、その主語は「nous」または「on」になり、動詞には「être」を用いる。
-
-- Nous sommes le lundi 3 janvier 2022. (2022年1月3日月曜日です。)
-
-「〜日」は通常基数で表すが、「1日」だけ例外で序数「premier」を使う。
diff --git a/app/learn/french/08/page.mdx b/app/learn/french/08/page.mdx
deleted file mode 100644
index 13452e0..0000000
--- a/app/learn/french/08/page.mdx
+++ /dev/null
@@ -1,25 +0,0 @@
-export const title = "人称代名詞";
-
-# 人称代名詞
-
-_第3章 人称詞と指示詞より_
-
-| | | 主語 | 直接目的語 | 間接目的語 | 強勢形 |
-| ---------- | --- | ----- | ---------- | ---------- | ------ |
-| 一人称単数 | | je | me | me | moi |
-| 二人称単数 | | tu | te | te | toi |
-| 三人称単数 | 男 | il | le | lui | lui |
-| 三人称単数 | 女 | elle | la | lui | elle |
-| 一人称複数 | | nous | nous | nous | nous |
-| 二人称複数 | | vous | vous | vous | vous |
-| 三人称複数 | 男 | ils | les | leur | eux |
-| 三人称複数 | 女 | elles | les | leur | elles |
-
-- 強勢形は、前置詞の目的語や対比・強調などに用いられる。
-- 三人称の人称代名詞は非人称的な使い方で用いられることがある。
-- 目的語人称代名詞が再帰代名詞となる場合、三人称のものは「se」となる。
-- 不定主語の「on」(文法上は三人称単数)は「人は」「世間では」「一般に」「私たち」といった意味を持つ。
- - Il lit et l'on écoute. (彼が読み、人々が聞く。)
- - この「l'」は母音で終わる特定の単語の後で母音の衝突を避けるために使われ、文法的な意味はない。
-- 目的語代名詞は原則動詞の前に持ってくる(不定詞などの場合でも同様)。
-- 複数の目的語代名詞が並ぶときの順序:「一・二人称間接目的語 → 三人称直接目的語 → 三人称間接目的語」。一・二人称直接目的語は他の目的語と同時には使えない。
diff --git a/app/learn/french/09/page.mdx b/app/learn/french/09/page.mdx
deleted file mode 100644
index c29b720..0000000
--- a/app/learn/french/09/page.mdx
+++ /dev/null
@@ -1,17 +0,0 @@
-export const title = "所有形容詞";
-
-# 所有形容詞
-
-_第3章 人称詞と指示詞より_
-
-| | 単数(男) | 単数(女) | 複数 |
-| ---------- | ---------- | ---------- | ----- |
-| 一人称単数 | mon | ma (mon) | mes |
-| 二人称単数 | ton | ta (ton) | tes |
-| 三人称単数 | son | sa (son) | ses |
-| 一人称複数 | notre | notre | nos |
-| 二人称複数 | votre | votre | vos |
-| 三人称複数 | leur | leur | leurs |
-
-- 所有形容詞は、修飾する名詞によって形が変化する。
-- 修飾する語の先頭が母音時や無音のhの時、一部の女性形は括弧内のように変化する。
diff --git a/app/learn/french/10/page.mdx b/app/learn/french/10/page.mdx
deleted file mode 100644
index 38022bd..0000000
--- a/app/learn/french/10/page.mdx
+++ /dev/null
@@ -1,16 +0,0 @@
-export const title = "所有代名詞";
-
-# 所有代名詞
-
-_第3章 人称詞と指示詞より_
-
-英語のmineなどにあたり、「〜のもの」と訳される。常に定冠詞とセットで、置き換える「もの」の性数によって形が変化する。
-
-| | 男性単数 | 女性単数 | 男性複数 | 女性複数 |
-| ---------- | -------- | --------- | ---------- | ----------- |
-| 一人称単数 | le mien | la mienne | les miens | les miennes |
-| 二人称単数 | le tien | la tienne | les tiens | les tiennes |
-| 三人称単数 | le sien | la sienne | les siens | les siennes |
-| 一人称複数 | le nôtre | la nôtre | les nôtres | les nôtres |
-| 二人称複数 | le vôtre | la vôtre | les vôtres | les vôtres |
-| 三人称複数 | le leur | la leur | les leurs | les leurs |
diff --git a/app/learn/french/11/page.mdx b/app/learn/french/11/page.mdx
deleted file mode 100644
index 8945c51..0000000
--- a/app/learn/french/11/page.mdx
+++ /dev/null
@@ -1,15 +0,0 @@
-export const title = "指示形容詞";
-
-# 指示形容詞
-
-_第3章 人称詞と指示詞より_
-
-「この」「その」「あの」に相当し、フランス語ではこれらすべてが同じ単語で表される。
-
-| | 男性 | 女性 |
-| ---- | -------- | ----- |
-| 単数 | ce (cet) | cette |
-| 複数 | ces | ces |
-
-- 修飾する名詞が母音始まりの男性単数形の時、発音の都合上cetを用いる。
-- 近称と遠称を区別したいときは、修飾する名詞の後ろに-ci(近称)や-là(遠称)をつける。
diff --git a/app/learn/french/12/page.mdx b/app/learn/french/12/page.mdx
deleted file mode 100644
index b8e1d98..0000000
--- a/app/learn/french/12/page.mdx
+++ /dev/null
@@ -1,21 +0,0 @@
-export const title = "指示代名詞";
-
-# 指示代名詞
-
-_第3章 人称詞と指示詞より_
-
-## パターン1
-
-| | これ | それ |
-| -------- | ---- | -------- |
-| 無強勢形 | ce | ce |
-| 強勢形 | ceci | cela(ça) |
-
-## パターン2
-
-| | 男性 | 女性 |
-| ---- | ----- | ------ |
-| 単数 | celui | celle |
-| 複数 | ceux | celles |
-
-- 近称と遠称を区別したいときには、それぞれの後ろに-ci(近称)や-là(遠称)をつける。
diff --git a/app/learn/french/13/page.mdx b/app/learn/french/13/page.mdx
deleted file mode 100644
index fbfd347..0000000
--- a/app/learn/french/13/page.mdx
+++ /dev/null
@@ -1,20 +0,0 @@
-export const title = "主な否定表現";
-
-# 主な否定表現
-
-_第4章 否定文と疑問文より_
-
-| 表現 | 意味 | 例文 |
-| ------------- | ------------ | ---------------------------------------- |
-| ne~plus | もう〜ない | Je n'habite plus à Lyon. |
-| ne~jamais | 決して〜ない | Je ne travaille jamais le samedi. |
-| ne~rien | 何も〜ない | Ce n'est rien. |
-| ne~personne | 誰も〜ない | Il n'y a personne. |
-| ne~ni A ni B | AもBも〜ない | Je ne parle ni l'anglais ni le japonais. |
-
-## 疑問詞のない疑問文への答え方
-
-- 肯定疑問文:Oui(はい)/Non(いいえ)
-- 否定疑問文:Si(いいえ)/Non(はい)
-
-「si」は否定文への、「non」は肯定文への付加疑問文の付加要素としても同様に作用する。
diff --git a/app/learn/french/14/page.mdx b/app/learn/french/14/page.mdx
deleted file mode 100644
index 8b54b0c..0000000
--- a/app/learn/french/14/page.mdx
+++ /dev/null
@@ -1,22 +0,0 @@
-export const title = "疑問詞一覧";
-
-# 疑問詞一覧
-
-_第4章 否定文と疑問文より_
-
-| 疑問詞 | 意味 | 例文 |
-| -------- | ------ | ---------------------------------------- |
-| qui | 誰 | Qui est là ? |
-| que | 何 | Qu'est-ce que c'est ? |
-| quand | いつ | Quand allez-vous au Japon ? |
-| où | どこ | Où habites-tu ? |
-| combien | いくつ | Combien d'enfants avez-vous ? |
-| pourquoi | なぜ | Pourquoi pleures-tu ? |
-| comment | どう | Comment allez-vous ? |
-| quel | どの | Quelle voiture avez-vous achetée ? |
-| lequel | どれ | Laquelle de ces voitures préférez-vous ? |
-| quoi | 何 | Avec quoi mangez-vous ce plat ? |
-
-- quelとlequelは性数変化する。
-- quoiは前置詞の目的語部分を質問するときに使う。
-- 間接疑問文の場合「何が」は「ce qui」に、「何を」は「ce que」になる。
diff --git a/app/learn/french/15/page.mdx b/app/learn/french/15/page.mdx
deleted file mode 100644
index 26b7df1..0000000
--- a/app/learn/french/15/page.mdx
+++ /dev/null
@@ -1,22 +0,0 @@
-export const title = "命令形の不規則活用";
-
-# 命令形の不規則活用
-
-_第5章 命令文より_
-
-大半の動詞は直説法現在形と同型で命令形を作るが、以下の動詞は接続法の語形を元に命令形を作る。
-
-| | 二人称単数命令形 | 二人称複数命令形 | 一人称複数命令形 |
-| ------- | ---------------- | ---------------- | ---------------- |
-| être | sois | soyez | soyons |
-| avoir | aie | ayez | ayons |
-| savoir | sache | sachez | sachons |
-| vouloir | veuille | veuillez | veuillons |
-
-- veuilleとveuillonsについてはほとんど使用されない。
-- Ayez du courage ! (元気を出してください。)
-- Soyez patient ! (我慢してください。)
-
-肯定命令文において人称代名詞をつける場合は、動詞の後ろにおいてハイフンで結ぶ(この場合、人称代名詞は強勢形になる)。
-
-- Excusez-moi.
diff --git a/app/learn/french/16/page.mdx b/app/learn/french/16/page.mdx
deleted file mode 100644
index 6fed985..0000000
--- a/app/learn/french/16/page.mdx
+++ /dev/null
@@ -1,16 +0,0 @@
-export const title = "直説法半過去形の活用";
-
-# 直説法半過去形の活用
-
-_第6章 時制より_
-
-語幹は直説法現在形の一人称複数形の語幹と同じ(êtreだけ例外でét-)。
-
-| 人称 | 語尾 | 例(donner) |
-| ---------- | ------ | ------------ |
-| 一人称単数 | -ais | donnais |
-| 二人称単数 | -ais | donnais |
-| 三人称単数 | -ait | donnait |
-| 一人称複数 | -ions | donnions |
-| 二人称複数 | -iez | donniez |
-| 三人称複数 | -aient | donnaient |
diff --git a/app/learn/french/17/page.mdx b/app/learn/french/17/page.mdx
deleted file mode 100644
index 1416eba..0000000
--- a/app/learn/french/17/page.mdx
+++ /dev/null
@@ -1,20 +0,0 @@
-export const title = "直説法単純未来形の活用";
-
-# 直説法単純未来形の活用
-
-_第6章 時制より_
-
-語幹の作り方:
-
-- -er動詞:原型からrをとったものを語幹にする
-- -ir動詞:原型からrをとったものを語幹にする
-- reで終わる動詞:原型からreを取り除いたものを語幹にする
-
-| 人称 | 語尾 | 例(donner) | 例(finir) | 例(être) | 例(avoir) |
-| ---------- | -------- | ------------ | ----------- | ---------- | ----------- |
-| 一人称単数 | -(e)rai | donnerai | finirai | serai | aurai |
-| 二人称単数 | -(e)ras | donneras | finiras | seras | auras |
-| 三人称単数 | -(e)ra | donnera | finira | sera | aura |
-| 一人称複数 | -(e)rons | donnerons | finirons | serons | aurons |
-| 二人称複数 | -(e)rez | donnerez | finirez | serez | aurez |
-| 三人称複数 | -(e)ront | donneront | finiront | seront | auront |
diff --git a/app/learn/french/18/page.mdx b/app/learn/french/18/page.mdx
deleted file mode 100644
index 8a9d7cb..0000000
--- a/app/learn/french/18/page.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
-export const title = "比較級で特殊な変化をする形容詞・副詞";
-
-# 比較級で特殊な変化をする形容詞・副詞
-
-_第8章 比較より_
-
-- bonの優等比較級:meilleur(×plus bon)
-- bienの優等比較級:mieux(×plus bien)
-- mauvaisの優等比較級:pire(×plus mauvais)
-- petitの優等比較級:moindre(×plus petit)
diff --git a/app/learn/french/19/page.mdx b/app/learn/french/19/page.mdx
deleted file mode 100644
index 8821a82..0000000
--- a/app/learn/french/19/page.mdx
+++ /dev/null
@@ -1,20 +0,0 @@
-export const title = "現在分詞・過去分詞の作り方";
-
-# 現在分詞・過去分詞の作り方
-
-_第10章 分詞より_
-
-## 現在分詞
-
-動詞の1人称複数形の語幹に、-antを付ける。
-
-- parler → parlant
-
-例外:être → étant、avoir → ayant、savoir → sachant
-
-## 過去分詞
-
-- er動詞:語尾をéにする(manger → mangé)
-- ir動詞:語尾をiにする(finir → fini)
-- être → été
-- avoir → eu
diff --git a/app/learn/french/20/page.mdx b/app/learn/french/20/page.mdx
deleted file mode 100644
index a342929..0000000
--- a/app/learn/french/20/page.mdx
+++ /dev/null
@@ -1,18 +0,0 @@
-export const title = "条件法現在形の活用";
-
-# 条件法現在形の活用
-
-_第11章 条件法より_
-
-作り方:「単純未来形の前半部分(rまで)+半過去形の語尾」
-
-| 人称 | 例(donner) | 例(finir) | 例(être) | 例(avoir) | 例(faire) |
-| ---------- | ------------ | ----------- | ---------- | ----------- | ----------- |
-| 一人称単数 | donnerais | finirais | serais | aurais | ferais |
-| 二人称単数 | donnerais | finirais | serais | aurais | ferais |
-| 三人称単数 | donnerait | finirait | serait | aurait | ferait |
-| 一人称複数 | donnerions | finirions | serions | aurions | ferions |
-| 二人称複数 | donneriez | finiriez | seriez | auriez | feriez |
-| 三人称複数 | donneraient | finiraient | seraient | auraient | feraient |
-
-- 条件法過去形は「avoir(être)の条件法現在形+動詞の過去分詞形」で作成可能。
diff --git a/app/learn/french/21/page.mdx b/app/learn/french/21/page.mdx
deleted file mode 100644
index e017fd7..0000000
--- a/app/learn/french/21/page.mdx
+++ /dev/null
@@ -1,17 +0,0 @@
-export const title = "接続法現在形の活用";
-
-# 接続法現在形の活用
-
-_第12章 接続法より_
-
-| 人称 | 語幹 | 語尾 | 特殊形(être) | 特殊形(avoir) |
-| ---------- | ------------------------------------ | ----- | -------------- | --------------- |
-| 一人称単数 | 直説法現在形3人称複数形の語幹と同様 | -e | sois | aie |
-| 二人称単数 | 直説法現在形3人称複数形の語幹と同様 | -es | sois | aies |
-| 三人称単数 | 直説法現在形3人称複数形の語幹と同様 | -e | soit | ait |
-| 一人称複数 | 直説法現在形1人称複数形の語幹と同様 | -ions | soyons | ayons |
-| 二人称複数 | 直説法現在形1人称複数形の語幹と同様 | -iez | soyez | ayez |
-| 三人称複数 | 直説法現在形3人称複数形の語幹と同様 | -ent | soient | aient |
-
-- 語幹は不規則でこの通りにならないものもあるが、語尾は規則的である。
-- 単数形と三人称複数形の語尾は直説法現在形と同じで、一人称複数、二人称複数の語尾は半過去形と同じである。
diff --git a/app/learn/french/22/page.mdx b/app/learn/french/22/page.mdx
deleted file mode 100644
index 3471559..0000000
--- a/app/learn/french/22/page.mdx
+++ /dev/null
@@ -1,23 +0,0 @@
-export const title = "不定表現一覧";
-
-# 不定表現一覧
-
-_第13章 不定表現より_
-
-| 不定表現 | 意味 | 補足 |
-| --------- | ---------------------------- | ------------------------------------------------------------------------ |
-| aucun | いかなる(〜ない) | 否定文で用いられる/原則単数名詞を修飾する/pasと共には使えない |
-| quelque | いくつかの、いくらかの/ある | 前者は可算複数形または不可算名詞、後者は可算単数形または不可算名詞を伴う |
-| n'importe | 〜でも(よい) | 後ろには疑問詞が続く |
-| chaque | それぞれの | 不定形容詞であり、複数形は存在しない |
-| chacun | それぞれの | 不定代名詞であり、複数形は存在しない |
-| tout | すべての | 後ろに定冠詞付きの名詞を伴ってそれを修飾する |
-
-- Il n'y a aucune solution à ce problème.
-- J'ai encore quelques questions à vous poser.
-- Il a montré quelque intérêt pour ce projet.
-- Vous pouvez appeler n'importe quand, je suis disponible.
-- Choisissez n'importe quel livre, ils sont tous intéressants.
-- Chaque étudiant doit rendre son devoir avant vendredi.
-- J'ai distribué des cartes : une pour chacun.
-- Toutes les fenêtres doivent rester fermées.
diff --git a/app/learn/french/23/page.mdx b/app/learn/french/23/page.mdx
deleted file mode 100644
index eab63d7..0000000
--- a/app/learn/french/23/page.mdx
+++ /dev/null
@@ -1,17 +0,0 @@
-export const title = "中性代名詞一覧";
-
-# 中性代名詞一覧
-
-_第16章 中性代名詞より_
-
-中性代名詞は「性数一致しない代名詞」のこと。
-
-| 中性代名詞 | 指示対象 | 例文 |
-| ---------- | -------------------------- | ------------------------------------------------------------------------------------------------- |
-| en | 前置詞「de」+定名詞(句) | J'en viens.(私はそこから来ました。) |
-| en | 同類を表す不定名詞(句) | Avez-vous des amis ? -Oui, j'en ai.(あなたは友達がいますか。-はい、います。) |
-| y | 前置詞「à」+定名詞(句) | On y compare la vie.(人生はそれにたとえられる。) |
-| le | 文脈全体 | Je lui ai téléphoné. -Je l'ai fait par précaution.(私は彼に電話をした。-私は念のためそうした。) |
-| le | 形容詞 | Êtes-vous fatigué ? -Oui, je le suis.(あなたは疲れていますか。-はい、疲れています。) |
-
-- enが同類を表す不定名詞(句)を置き換える場合で、後ろに形容詞が付いている場合は、形容詞は「de+形容詞」の形で残る。
diff --git a/app/learn/german/02/page.mdx b/app/learn/german/02/page.mdx
deleted file mode 100644
index f01e9cb..0000000
--- a/app/learn/german/02/page.mdx
+++ /dev/null
@@ -1,12 +0,0 @@
-export const title = "名詞の性の語尾による判別";
-
-# 名詞の性の語尾による判別
-
-_第1章 品詞と文型より_
-
-ものの名詞は男性・女性を直観的に判断できないため、語尾によってある程度判別する。(例外もある)
-
-| 語尾 | 性 |
-| ------------------------------- | -------- |
-| -age、-ment、-eau、-teur | 男性名詞 |
-| -tion、-sion、-té、-esse、-ette | 女性名詞 |
diff --git a/app/learn/german/02/page.tsx b/app/learn/german/02/page.tsx
new file mode 100644
index 0000000..6095a45
--- /dev/null
+++ b/app/learn/german/02/page.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import { TranslationPractice } from "@/components/translation/TranslationPractice";
+
+export const title = "翻訳";
+
+export default function GermanTranslationPage() {
+ return (
+
+
翻訳
+
+ ドイツ語⇔日本語の訳文を書き、AIに添削してもらいましょう。
+
+
+
+
+
+ );
+}
diff --git a/app/learn/spanish/02/page.mdx b/app/learn/spanish/02/page.mdx
deleted file mode 100644
index 3d3f536..0000000
--- a/app/learn/spanish/02/page.mdx
+++ /dev/null
@@ -1,51 +0,0 @@
-import SpanishNumberPracticeLoader from "@/components/spanish/SpanishNumberPracticeLoader";
-
-export const title = "数字タイピング演習";
-
-# 数字タイピング演習
-
-
-
-{/* 出題データ定義テーブル (display:none) */}
-
-
-
-| 数 | 綴り |
-| --- | ------------ |
-| 0 | cero |
-| 1 | uno |
-| 2 | dos |
-| 3 | tres |
-| 4 | cuatro |
-| 5 | cinco |
-| 6 | seis |
-| 7 | siete |
-| 8 | ocho |
-| 9 | nueve |
-| 10 | diez |
-| 11 | once |
-| 12 | doce |
-| 13 | trece |
-| 14 | catorce |
-| 15 | quince |
-| 16 | dieciséis |
-| 17 | diecisiete |
-| 18 | dieciocho |
-| 19 | diecinueve |
-| 20 | veinte |
-| 21 | veintiuno |
-| 22 | veintidós |
-| 23 | veintitrés |
-| 24 | veinticuatro |
-| 25 | veinticinco |
-| 26 | veintiséis |
-| 30 | treinta |
-| 40 | cuarenta |
-| 50 | cincuenta |
-| 60 | sesenta |
-| 70 | setenta |
-| 80 | ochenta |
-| 90 | noventa |
-| 100 | cien |
-
-
diff --git a/app/learn/spanish/02/page.tsx b/app/learn/spanish/02/page.tsx
index 163113e..3bd940f 100644
--- a/app/learn/spanish/02/page.tsx
+++ b/app/learn/spanish/02/page.tsx
@@ -1,18 +1,18 @@
"use client";
-import { ConjugationPractice } from "@/components/verbs/ConjugationPractice";
-import { spanishConjugationConfig } from "@/lib/conjugation/es/config";
-import { verbs } from "./verbs";
+import { TranslationPractice } from "@/components/translation/TranslationPractice";
-export const title = "動詞の活用";
+export const title = "翻訳";
-export default function SpanishVerbConjugationPage() {
+export default function SpanishTranslationPage() {
return (
-
動詞の活用
-
動詞・時制・法を選んで、活用形を入力しながら覚えましょう。
+
翻訳
+
+ スペイン語⇔日本語の訳文を書き、AIに添削してもらいましょう。
+
-
+
);
diff --git a/app/learn/spanish/03/page.mdx b/app/learn/spanish/03/page.mdx
index 6c31ae1..3d3f536 100644
--- a/app/learn/spanish/03/page.mdx
+++ b/app/learn/spanish/03/page.mdx
@@ -1,67 +1,51 @@
-import SpanishTablePracticeLoader from "@/components/spanish/SpanishTablePracticeLoader";
+import SpanishNumberPracticeLoader from "@/components/spanish/SpanishNumberPracticeLoader";
-export const title = "人称代名詞・冠詞・指示詞の表完成演習";
+export const title = "数字タイピング演習";
-# 人称代名詞・冠詞・指示詞の表完成演習
+# 数字タイピング演習
-
+
{/* 出題データ定義テーブル (display:none) */}
-### 人称代名詞 (主格・目的格・強勢形)
-
-| 人称・性数 | 主格 (〜は) | 直接目的格 (〜を) | 間接目的格 (〜に) | 強勢形 (前置詞の後) | 再帰代名詞 (自分を/に) |
-| ------------- | ----------- | ----------------- | ----------------- | ------------------- | ---------------------- |
-| 1人称単数 | yo | me | me | mí | me |
-| 2人称単数 | tú | te | te | ti | te |
-| 3人称単数男性 | él | lo | le | él | se |
-| 3人称単数女性 | ella | la | le | ella | se |
-| 3人称単数敬称 | usted | lo | le | usted | se |
-| 1人称複数男性 | nosotros | nos | nos | nosotros | nos |
-| 1人称複数女性 | nosotras | nos | nos | nosotras | nos |
-| 2人称複数男性 | vosotros | os | os | vosotros | os |
-| 2人称複数女性 | vosotras | os | os | vosotras | os |
-| 3人称複数男性 | ellos | los | les | ellos | se |
-| 3人称複数女性 | ellas | las | les | ellas | se |
-| 3人称複数敬称 | ustedes | los | les | ustedes | se |
-
-### 所有形容詞 (前置形: 名詞の前)
-
-| 人称・所有者 | 男性単数名詞を修飾 | 女性単数名詞を修飾 | 男性複数名詞を修飾 | 女性複数名詞を修飾 |
-| ---------------------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ |
-| 1人称単数 (私の) | mi | mi | mis | mis |
-| 2人称単数 (君の) | tu | tu | tus | tus |
-| 3人称単数・敬称 (彼の/彼女の/あなたの) | su | su | sus | sus |
-| 1人称複数男性 (私たちの) | nuestro | nuestra | nuestros | nuestras |
-| 2人称複数男性 (君たちの) | vuestro | vuestra | vuestros | vuestras |
-| 3人称複数・敬称 (彼らの/彼女らの/あなたがたの) | su | su | sus | sus |
-
-### 所有代名詞 (〜のもの)
-
-| 人称・所有者 | 男性単数 (〜の者/物) | 女性単数 | 男性複数 | 女性複数 |
-| ------------------------------------------ | -------------------- | -------- | -------- | -------- |
-| 1人称単数 (私のもの) | mío | mía | míos | mías |
-| 2人称単数 (君のもの) | tuyo | tuya | tuyos | tuyas |
-| 3人称単数・敬称 (彼の/彼女の/あなたのもの) | suyo | suya | suyos | suyas |
-| 1人称複数 (私たちのもの) | nuestro | nuestra | nuestros | nuestras |
-| 2人称複数 (君たちのもの) | vuestro | vuestra | vuestros | vuestras |
-| 3人称複数 (彼らの/あなたがたのもの) | suyo | suya | suyos | suyas |
-
-### 冠詞 (定冠詞・不定冠詞)
-
-| 冠詞の種類 | 男性単数 | 女性単数 | 男性複数 | 女性複数 |
-| -------------------------- | -------- | -------- | -------- | -------- |
-| 定冠詞 (その) | el | la | los | las |
-| 不定冠詞 (ある/いくつかの) | un | una | unos | unas |
-
-### 指示詞 (指示形容詞・指示代名詞)
-
-| 位置・距離 | 男性単数 | 女性単数 | 男性複数 | 女性複数 | 中性 |
-| ------------------ | -------- | -------- | -------- | -------- | ------- |
-| これ / この (近称) | este | esta | estos | estas | esto |
-| それ / その (中称) | ese | esa | esos | esas | eso |
-| あれ / あの (遠称) | aquel | aquella | aquellos | aquellas | aquello |
+| 数 | 綴り |
+| --- | ------------ |
+| 0 | cero |
+| 1 | uno |
+| 2 | dos |
+| 3 | tres |
+| 4 | cuatro |
+| 5 | cinco |
+| 6 | seis |
+| 7 | siete |
+| 8 | ocho |
+| 9 | nueve |
+| 10 | diez |
+| 11 | once |
+| 12 | doce |
+| 13 | trece |
+| 14 | catorce |
+| 15 | quince |
+| 16 | dieciséis |
+| 17 | diecisiete |
+| 18 | dieciocho |
+| 19 | diecinueve |
+| 20 | veinte |
+| 21 | veintiuno |
+| 22 | veintidós |
+| 23 | veintitrés |
+| 24 | veinticuatro |
+| 25 | veinticinco |
+| 26 | veintiséis |
+| 30 | treinta |
+| 40 | cuarenta |
+| 50 | cincuenta |
+| 60 | sesenta |
+| 70 | setenta |
+| 80 | ochenta |
+| 90 | noventa |
+| 100 | cien |
diff --git a/app/learn/spanish/03/page.tsx b/app/learn/spanish/03/page.tsx
new file mode 100644
index 0000000..163113e
--- /dev/null
+++ b/app/learn/spanish/03/page.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import { ConjugationPractice } from "@/components/verbs/ConjugationPractice";
+import { spanishConjugationConfig } from "@/lib/conjugation/es/config";
+import { verbs } from "./verbs";
+
+export const title = "動詞の活用";
+
+export default function SpanishVerbConjugationPage() {
+ return (
+
+
動詞の活用
+
動詞・時制・法を選んで、活用形を入力しながら覚えましょう。
+
+
+
+
+ );
+}
diff --git a/app/learn/spanish/02/verbs.ts b/app/learn/spanish/03/verbs.ts
similarity index 100%
rename from app/learn/spanish/02/verbs.ts
rename to app/learn/spanish/03/verbs.ts
diff --git a/app/learn/spanish/04/page.mdx b/app/learn/spanish/04/page.mdx
index c16bc0f..6c31ae1 100644
--- a/app/learn/spanish/04/page.mdx
+++ b/app/learn/spanish/04/page.mdx
@@ -1,44 +1,67 @@
-import SpanishFillInPracticeLoader from "@/components/spanish/SpanishFillInPracticeLoader";
+import SpanishTablePracticeLoader from "@/components/spanish/SpanishTablePracticeLoader";
-{/* セクションタイトルの定義(サイドバーや一覧ページで自動取得・表示される) */}
-export const title = "紛らわしい文法(ser/estar・点過去/線過去)穴埋め演習";
+export const title = "人称代名詞・冠詞・指示詞の表完成演習";
-# 紛らわしい文法(ser/estar・点過去/線過去)穴埋め演習
+# 人称代名詞・冠詞・指示詞の表完成演習
-{/* 穴埋め演習用コンポーネントの読み込み */}
+
-
+{/* 出題データ定義テーブル (display:none) */}
-### ser と estar の使い分け
-
-| 例文 (問題文) | 日本語訳・ヒント | 正解 | 解説 |
-| ------------------------- | -------------------------------- | ------- | ------------------------------------------------------------------ |
-| Ella \_ profesora. | 彼女は教員です。 | es | 職業・身分を表すため ser(三名単: es)を使用します。 |
-| Hoy \_ nublado. | 今日は曇っています。 | está | 一時的な天候・状態を表すため estar(三名単: está)を使用します。 |
-| Nosotros \_ en el parque. | 私たちは公園にいます。 | estamos | 居場所・所在を表すため estar(一人複: estamos)を使用します。 |
-| Juan \_ alto y simpático. | フアンは背が高くて親切です。 | es | 性質・特徴を表すため ser(三名単: es)を使用します。 |
-| La sopa \_ muy fría. | スープがとても冷えています。 | está | 一時的な状態(温度)を表すため estar(三名単: está)を使用します。 |
-| Madrid \_ en España. | マドリードはスペインにあります。 | está | 位置・所在を表すため estar(三名単: está)を使用します。 |
-| Mis padres \_ abogados. | 私の両親は弁護士です。 | son | 職業・身分を表すため ser(三人複: son)を使用します。 |
-| ¿Dónde \_ las llaves? | 鍵はどこにありますか? | están | 物の所在を表すため estar(三人複: están)を使用します。 |
-| Mañana \_ domingo. | 明日は日曜日です。 | es | 日時・曜日を表すため ser(三名単: es)を使用します。 |
-| El café \_ muy caliente. | コーヒーがとても熱いです。 | está | 一時的な状態(温度)を表すため estar(三名単: está)を使用します。 |
-
-### 線過去 と 点過去 の使い分け
-
-| 例文 (問題文) | 日本語訳・ヒント | 正解 | 解説 |
-| ----------------------------------- | --------------------------------------------------- | ------ | -------------------------------------------------------------------------- |
-| Yo \_ con María ayer. | 私は昨日マリアと話した(hablar)。 | hablé | 「昨日(ayer)」完了した特定の過去の動作のため点過去を使用します。 |
-| Cuando yo \_ niño, vivía en Madrid. | 私が子供だったとき(ser)、マドリードに住んでいた。 | era | 過去の継続的な状態(子供時代)を表すため線過去を使用します。 |
-| Ayer \_ un día maravilloso. | 昨日は素晴らしい一日だった(ser)。 | fue | 「昨日(ayer)」という完了した期間の事実のため点過去を使用します。 |
-| Mientras yo estudiaba, él \_. | 私は勉強していた間、彼は寝ていた(dormir)。 | dormía | 並行して続いていた過去の動作を表すため線過去を使用します。 |
-| Todos los días yo \_ al parque. | 毎日私は公園に行っていた(ir)。 | iba | 「毎日(Todos los días)」繰り返された過去の習慣のため線過去を使用します。 |
-| De repente, \_ a llover. | 突然、雨が降り始めた(empezar)。 | empezó | 「突然(De repente)」発生した過去の出来事・起点の文脈のため点過去を使用。 |
-| Cuando la vi, ella \_ 10 años. | 私が彼女に会ったとき、彼女は10歳だった(tener)。 | tenía | 過去の年齢・状態の背景描写のため線過去を使用します。 |
-| El año pasado \_ a España. | 去年、スペインへ旅行した(viajar)。 | viajé | 「去年(El año pasado)」完了した過去の出来事のため点過去を使用します。 |
-| Siempre \_ en la calle. | いつも通りで遊んでいた(jugar)。 | jugaba | 「いつも(Siempre)」行っていた過去の習慣・反復動作のため線過去を使用。 |
-| Ayer \_ mucho frío. | 昨日はとても寒かった(hacer)。 | hizo | 「昨日(ayer)」完了した過去の特定の天候・気候状態のため点過去を使用。 |
+### 人称代名詞 (主格・目的格・強勢形)
+
+| 人称・性数 | 主格 (〜は) | 直接目的格 (〜を) | 間接目的格 (〜に) | 強勢形 (前置詞の後) | 再帰代名詞 (自分を/に) |
+| ------------- | ----------- | ----------------- | ----------------- | ------------------- | ---------------------- |
+| 1人称単数 | yo | me | me | mí | me |
+| 2人称単数 | tú | te | te | ti | te |
+| 3人称単数男性 | él | lo | le | él | se |
+| 3人称単数女性 | ella | la | le | ella | se |
+| 3人称単数敬称 | usted | lo | le | usted | se |
+| 1人称複数男性 | nosotros | nos | nos | nosotros | nos |
+| 1人称複数女性 | nosotras | nos | nos | nosotras | nos |
+| 2人称複数男性 | vosotros | os | os | vosotros | os |
+| 2人称複数女性 | vosotras | os | os | vosotras | os |
+| 3人称複数男性 | ellos | los | les | ellos | se |
+| 3人称複数女性 | ellas | las | les | ellas | se |
+| 3人称複数敬称 | ustedes | los | les | ustedes | se |
+
+### 所有形容詞 (前置形: 名詞の前)
+
+| 人称・所有者 | 男性単数名詞を修飾 | 女性単数名詞を修飾 | 男性複数名詞を修飾 | 女性複数名詞を修飾 |
+| ---------------------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ |
+| 1人称単数 (私の) | mi | mi | mis | mis |
+| 2人称単数 (君の) | tu | tu | tus | tus |
+| 3人称単数・敬称 (彼の/彼女の/あなたの) | su | su | sus | sus |
+| 1人称複数男性 (私たちの) | nuestro | nuestra | nuestros | nuestras |
+| 2人称複数男性 (君たちの) | vuestro | vuestra | vuestros | vuestras |
+| 3人称複数・敬称 (彼らの/彼女らの/あなたがたの) | su | su | sus | sus |
+
+### 所有代名詞 (〜のもの)
+
+| 人称・所有者 | 男性単数 (〜の者/物) | 女性単数 | 男性複数 | 女性複数 |
+| ------------------------------------------ | -------------------- | -------- | -------- | -------- |
+| 1人称単数 (私のもの) | mío | mía | míos | mías |
+| 2人称単数 (君のもの) | tuyo | tuya | tuyos | tuyas |
+| 3人称単数・敬称 (彼の/彼女の/あなたのもの) | suyo | suya | suyos | suyas |
+| 1人称複数 (私たちのもの) | nuestro | nuestra | nuestros | nuestras |
+| 2人称複数 (君たちのもの) | vuestro | vuestra | vuestros | vuestras |
+| 3人称複数 (彼らの/あなたがたのもの) | suyo | suya | suyos | suyas |
+
+### 冠詞 (定冠詞・不定冠詞)
+
+| 冠詞の種類 | 男性単数 | 女性単数 | 男性複数 | 女性複数 |
+| -------------------------- | -------- | -------- | -------- | -------- |
+| 定冠詞 (その) | el | la | los | las |
+| 不定冠詞 (ある/いくつかの) | un | una | unos | unas |
+
+### 指示詞 (指示形容詞・指示代名詞)
+
+| 位置・距離 | 男性単数 | 女性単数 | 男性複数 | 女性複数 | 中性 |
+| ------------------ | -------- | -------- | -------- | -------- | ------- |
+| これ / この (近称) | este | esta | estos | estas | esto |
+| それ / その (中称) | ese | esa | esos | esas | eso |
+| あれ / あの (遠称) | aquel | aquella | aquellos | aquellas | aquello |
diff --git a/app/learn/spanish/05/page.mdx b/app/learn/spanish/05/page.mdx
new file mode 100644
index 0000000..c16bc0f
--- /dev/null
+++ b/app/learn/spanish/05/page.mdx
@@ -0,0 +1,44 @@
+import SpanishFillInPracticeLoader from "@/components/spanish/SpanishFillInPracticeLoader";
+
+{/* セクションタイトルの定義(サイドバーや一覧ページで自動取得・表示される) */}
+export const title = "紛らわしい文法(ser/estar・点過去/線過去)穴埋め演習";
+
+# 紛らわしい文法(ser/estar・点過去/線過去)穴埋め演習
+
+{/* 穴埋め演習用コンポーネントの読み込み */}
+
+
+
+
+
+### ser と estar の使い分け
+
+| 例文 (問題文) | 日本語訳・ヒント | 正解 | 解説 |
+| ------------------------- | -------------------------------- | ------- | ------------------------------------------------------------------ |
+| Ella \_ profesora. | 彼女は教員です。 | es | 職業・身分を表すため ser(三名単: es)を使用します。 |
+| Hoy \_ nublado. | 今日は曇っています。 | está | 一時的な天候・状態を表すため estar(三名単: está)を使用します。 |
+| Nosotros \_ en el parque. | 私たちは公園にいます。 | estamos | 居場所・所在を表すため estar(一人複: estamos)を使用します。 |
+| Juan \_ alto y simpático. | フアンは背が高くて親切です。 | es | 性質・特徴を表すため ser(三名単: es)を使用します。 |
+| La sopa \_ muy fría. | スープがとても冷えています。 | está | 一時的な状態(温度)を表すため estar(三名単: está)を使用します。 |
+| Madrid \_ en España. | マドリードはスペインにあります。 | está | 位置・所在を表すため estar(三名単: está)を使用します。 |
+| Mis padres \_ abogados. | 私の両親は弁護士です。 | son | 職業・身分を表すため ser(三人複: son)を使用します。 |
+| ¿Dónde \_ las llaves? | 鍵はどこにありますか? | están | 物の所在を表すため estar(三人複: están)を使用します。 |
+| Mañana \_ domingo. | 明日は日曜日です。 | es | 日時・曜日を表すため ser(三名単: es)を使用します。 |
+| El café \_ muy caliente. | コーヒーがとても熱いです。 | está | 一時的な状態(温度)を表すため estar(三名単: está)を使用します。 |
+
+### 線過去 と 点過去 の使い分け
+
+| 例文 (問題文) | 日本語訳・ヒント | 正解 | 解説 |
+| ----------------------------------- | --------------------------------------------------- | ------ | -------------------------------------------------------------------------- |
+| Yo \_ con María ayer. | 私は昨日マリアと話した(hablar)。 | hablé | 「昨日(ayer)」完了した特定の過去の動作のため点過去を使用します。 |
+| Cuando yo \_ niño, vivía en Madrid. | 私が子供だったとき(ser)、マドリードに住んでいた。 | era | 過去の継続的な状態(子供時代)を表すため線過去を使用します。 |
+| Ayer \_ un día maravilloso. | 昨日は素晴らしい一日だった(ser)。 | fue | 「昨日(ayer)」という完了した期間の事実のため点過去を使用します。 |
+| Mientras yo estudiaba, él \_. | 私は勉強していた間、彼は寝ていた(dormir)。 | dormía | 並行して続いていた過去の動作を表すため線過去を使用します。 |
+| Todos los días yo \_ al parque. | 毎日私は公園に行っていた(ir)。 | iba | 「毎日(Todos los días)」繰り返された過去の習慣のため線過去を使用します。 |
+| De repente, \_ a llover. | 突然、雨が降り始めた(empezar)。 | empezó | 「突然(De repente)」発生した過去の出来事・起点の文脈のため点過去を使用。 |
+| Cuando la vi, ella \_ 10 años. | 私が彼女に会ったとき、彼女は10歳だった(tener)。 | tenía | 過去の年齢・状態の背景描写のため線過去を使用します。 |
+| El año pasado \_ a España. | 去年、スペインへ旅行した(viajar)。 | viajé | 「去年(El año pasado)」完了した過去の出来事のため点過去を使用します。 |
+| Siempre \_ en la calle. | いつも通りで遊んでいた(jugar)。 | jugaba | 「いつも(Siempre)」行っていた過去の習慣・反復動作のため線過去を使用。 |
+| Ayer \_ mucho frío. | 昨日はとても寒かった(hacer)。 | hizo | 「昨日(ayer)」完了した過去の特定の天候・気候状態のため点過去を使用。 |
+
+
diff --git a/components/pinyin/PinyinPractice.tsx b/components/pinyin/PinyinPractice.tsx
index 5f8cb21..c4b43d5 100644
--- a/components/pinyin/PinyinPractice.tsx
+++ b/components/pinyin/PinyinPractice.tsx
@@ -3,7 +3,7 @@
import { useEffect, useMemo, useState } from "react";
import { AccentInput } from "@/components/verbs/AccentInput";
import { speak } from "@/lib/speech";
-import type { HanziEntry } from "@/app/learn/chinese/02/characters";
+import type { HanziEntry } from "@/app/learn/chinese/03/characters";
// 母音ごとの声調サイクル。母音を入力した直後に ↑/↓ キーで
// 第1声→第2声→第3声→第4声(→軽声)と切り替えられる。
diff --git a/components/spanish/SpanishFillInPracticeLoader.tsx b/components/spanish/SpanishFillInPracticeLoader.tsx
index b2df466..2934e7e 100644
--- a/components/spanish/SpanishFillInPracticeLoader.tsx
+++ b/components/spanish/SpanishFillInPracticeLoader.tsx
@@ -3,12 +3,12 @@ import path from "node:path";
import SpanishFillInPractice, { type FillInQuestionItem } from "./SpanishFillInPractice";
/**
- * MDX教材ファイル(app/learn/spanish/04/page.mdx)から
+ * MDX教材ファイル(app/learn/spanish/05/page.mdx)から
* 穴埋め文法問題データを動的に抽出する関数。
* データのベタ打ちを排除し、教材ファイルを唯一のデータソースとして使用する。
*/
async function getFillInQuestionsFromMdx(): Promise {
- const filePath = path.join(process.cwd(), "app", "learn", "spanish", "04", "page.mdx");
+ const filePath = path.join(process.cwd(), "app", "learn", "spanish", "05", "page.mdx");
const items: FillInQuestionItem[] = [];
try {
diff --git a/components/spanish/SpanishTablePracticeLoader.tsx b/components/spanish/SpanishTablePracticeLoader.tsx
index 46ce81a..4fc160d 100644
--- a/components/spanish/SpanishTablePracticeLoader.tsx
+++ b/components/spanish/SpanishTablePracticeLoader.tsx
@@ -22,12 +22,12 @@ function mergeAdjacentCells(cells: string[]): GrammarTableCell[] {
}
/**
- * MDX教材ファイル(app/learn/spanish/03/page.mdx)から
+ * MDX教材ファイル(app/learn/spanish/04/page.mdx)から
* 人称代名詞・冠詞・指示詞の各文法表データを動的に抽出する関数。
* データのベタ打ちを排除し、教材ファイルを唯一のデータソースとして使用する。
*/
async function getGrammarTablesFromMdx(): Promise {
- const filePath = path.join(process.cwd(), "app", "learn", "spanish", "03", "page.mdx");
+ const filePath = path.join(process.cwd(), "app", "learn", "spanish", "04", "page.mdx");
const tables: GrammarTableData[] = [];
try {
diff --git a/components/translation/TranslationPractice.tsx b/components/translation/TranslationPractice.tsx
new file mode 100644
index 0000000..e672220
--- /dev/null
+++ b/components/translation/TranslationPractice.tsx
@@ -0,0 +1,530 @@
+"use client";
+
+import { useEffect, useMemo, useState, useTransition } from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+import { getTranslationHistory, submitTranslation } from "@/lib/translation/actions";
+import { getTranslationLevels, getTranslationProblems } from "@/lib/translation/problems";
+import {
+ CUSTOM_SOURCE_MAX_LENGTH,
+ TRANSLATION_SCORE_DEFAULT,
+ TRANSLATION_SCORE_OPTIONS,
+ type TranslationDirection,
+ type TranslationProblemSource,
+ type TranslationSubmissionDTO,
+} from "@/lib/translation/types";
+
+interface ResolvedProblem {
+ source: TranslationProblemSource;
+ problemId?: string;
+ level: string;
+ direction: TranslationDirection;
+ sourceText: string;
+ referenceTranslation: string | null;
+}
+
+interface TranslationPracticeProps {
+ languageSlug: string;
+ languageLabel: string;
+}
+
+function directionLabel(direction: TranslationDirection, languageLabel: string): string {
+ return direction === "fromJapanese" ? `日本語 → ${languageLabel}` : `${languageLabel} → 日本語`;
+}
+
+const dateFormatter = new Intl.DateTimeFormat("ja-JP", {
+ dateStyle: "medium",
+ timeStyle: "short",
+});
+
+export function TranslationPractice({ languageSlug, languageLabel }: TranslationPracticeProps) {
+ const problems = getTranslationProblems(languageSlug);
+ const levels = useMemo(() => getTranslationLevels(languageSlug), [languageSlug]);
+
+ const [status, setStatus] = useState<"loading" | "guest" | "ready">("loading");
+ const [phase, setPhase] = useState<"settings" | "writing" | "result">("settings");
+ const [level, setLevel] = useState(levels[0] ?? "");
+ const [direction, setDirection] = useState("fromJapanese");
+ const [problemSource, setProblemSource] = useState("preset");
+ const [scoreMax, setScoreMax] = useState(TRANSLATION_SCORE_DEFAULT);
+
+ const problemsForSelection = useMemo(
+ () => problems.filter((p) => p.level === level && p.direction === direction),
+ [problems, level, direction],
+ );
+ const [selectedProblemId, setSelectedProblemId] = useState(problemsForSelection[0]?.id ?? "");
+ const [customSourceText, setCustomSourceText] = useState("");
+
+ const [activeProblem, setActiveProblem] = useState(null);
+ const [content, setContent] = useState("");
+ const [error, setError] = useState(null);
+ const [pending, startTransition] = useTransition();
+ const [lastSubmission, setLastSubmission] = useState(null);
+ const [history, setHistory] = useState([]);
+ const [expandedHistoryId, setExpandedHistoryId] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ getTranslationHistory(languageSlug).then((result) => {
+ if (cancelled) return;
+ setHistory(result.submissions);
+ setStatus(result.isLoggedIn ? "ready" : "guest");
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [languageSlug]);
+
+ const selectLevel = (nextLevel: string) => {
+ setLevel(nextLevel);
+ const nextProblems = problems.filter((p) => p.level === nextLevel && p.direction === direction);
+ setSelectedProblemId(nextProblems[0]?.id ?? "");
+ };
+
+ const selectDirection = (nextDirection: TranslationDirection) => {
+ setDirection(nextDirection);
+ const nextProblems = problems.filter((p) => p.level === level && p.direction === nextDirection);
+ setSelectedProblemId(nextProblems[0]?.id ?? "");
+ };
+
+ const canStart =
+ problemSource === "preset" ? selectedProblemId !== "" : customSourceText.trim() !== "";
+
+ const startWriting = () => {
+ if (problemSource === "preset") {
+ const problem = problemsForSelection.find((p) => p.id === selectedProblemId);
+ if (!problem) return;
+ setActiveProblem({
+ source: "preset",
+ problemId: problem.id,
+ level: problem.level,
+ direction: problem.direction,
+ sourceText: problem.sourceText,
+ referenceTranslation: problem.referenceTranslation,
+ });
+ } else {
+ const sourceText = customSourceText.trim();
+ if (!sourceText) return;
+ setActiveProblem({
+ source: "custom",
+ level,
+ direction,
+ sourceText,
+ referenceTranslation: null,
+ });
+ }
+ setContent("");
+ setError(null);
+ setLastSubmission(null);
+ setPhase("writing");
+ };
+
+ const backToSettings = () => {
+ setPhase("settings");
+ };
+
+ const handleSubmit = () => {
+ if (!activeProblem || pending) return;
+ if (!content.trim()) {
+ setError("訳文を入力してください。");
+ return;
+ }
+ setError(null);
+
+ startTransition(async () => {
+ const result = await submitTranslation({
+ language: languageSlug,
+ problemSource: activeProblem.source,
+ problemId: activeProblem.source === "preset" ? activeProblem.problemId : undefined,
+ direction: activeProblem.source === "custom" ? activeProblem.direction : undefined,
+ level: activeProblem.source === "custom" ? activeProblem.level : undefined,
+ customSourceText: activeProblem.source === "custom" ? activeProblem.sourceText : undefined,
+ content,
+ scoreMax,
+ });
+
+ if ("error" in result) {
+ setError(result.error);
+ return;
+ }
+
+ setLastSubmission(result.submission);
+ setHistory((prev) => [result.submission, ...prev]);
+ setPhase("result");
+ });
+ };
+
+ if (status === "loading") {
+ return 読み込み中…
;
+ }
+
+ if (status === "guest") {
+ return ;
+ }
+
+ return (
+
+ {phase === "settings" && (
+ <>
+
+ 級
+
+ {levels.map((l) => (
+
+ ))}
+ {levels.length === 0 && (
+
+ この言語にはまだ級が用意されていません。
+
+ )}
+
+
+
+
+ 訳す方向
+
+
+
+
+
+
+
+ 題材
+
+
+
+
+
+ {problemSource === "preset" ? (
+
+ {problemsForSelection.map((problem) => (
+
+ ))}
+ {problemsForSelection.length === 0 && (
+
+ この級・方向の問題はまだ用意されていません。「自分で題材を用意する」から始めてください。
+
+ )}
+
+ ) : (
+
+
+
+ )}
+
+
+
+ 採点の満点
+
+
+
+
+
+
+
+ {history.length > 0 && (
+
setExpandedHistoryId((prev) => (prev === id ? null : id))}
+ />
+ )}
+ >
+ )}
+
+ {phase === "writing" && activeProblem && (
+
+
+
+
+
+ {activeProblem.level} / {directionLabel(activeProblem.direction, languageLabel)}
+
+
{activeProblem.sourceText}
+
+
+
+ )}
+
+ {phase === "result" && lastSubmission && (
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+function TranslationFeedbackCard({
+ languageLabel,
+ submission,
+}: {
+ languageLabel: string;
+ submission: TranslationSubmissionDTO;
+}) {
+ const { feedback } = submission;
+ return (
+
+
+
+ {submission.level} / {directionLabel(submission.direction, languageLabel)}
+
+
+ {feedback.score} / {feedback.max}点
+
+
+
+
原文: {submission.sourceText}
+
+
+ {submission.content}
+
+
+ {feedback.comment && (
+
+ {feedback.comment}
+
+ )}
+
+ {submission.referenceTranslation && (
+
+ 模範解答(一例): {submission.referenceTranslation}
+
+ )}
+
+ );
+}
+
+function TranslationHistory({
+ languageLabel,
+ history,
+ expandedId,
+ onToggle,
+}: {
+ languageLabel: string;
+ history: TranslationSubmissionDTO[];
+ expandedId: string | null;
+ onToggle: (id: string) => void;
+}) {
+ return (
+
+ これまでの提出履歴
+
+ {history.map((submission) => {
+ const { feedback } = submission;
+ const expanded = expandedId === submission.id;
+ return (
+
+
+ {expanded && (
+
+
+
+ )}
+
+ );
+ })}
+
+
+ );
+}
+
+function TranslationLoginPrompt() {
+ const pathname = usePathname();
+ const redirectQuery = `redirect=${encodeURIComponent(pathname)}`;
+ return (
+
+
+ ログインして、翻訳セクションを使ってみましょう!
+
+
+ AIによる添削結果を保存して、いつでも見返せるようになります。
+
+
+
+ ログイン
+
+
+ 新規登録
+
+
+
+ );
+}
diff --git a/lib/translation/actions.ts b/lib/translation/actions.ts
new file mode 100644
index 0000000..1a3652b
--- /dev/null
+++ b/lib/translation/actions.ts
@@ -0,0 +1,159 @@
+"use server";
+
+import { prisma } from "@/lib/prisma";
+import type { Prisma } from "@/app/generated/prisma/client";
+import { getCurrentUser, requireUser } from "@/lib/session";
+import { getLanguage } from "@/app/learn/languages";
+import { getTranslationLevels, getTranslationProblem } from "./problems";
+import { gradeTranslation, TranslationGradingError } from "./grading";
+import { toTranslationSubmissionDTO } from "./dto";
+import {
+ CUSTOM_SOURCE_MAX_LENGTH,
+ TRANSLATION_SCORE_OPTIONS,
+ type TranslationDirection,
+ type TranslationProblemSource,
+ type TranslationSubmissionDTO,
+} from "./types";
+
+const CONTENT_MAX_LENGTH = 500;
+const HISTORY_LIMIT = 20;
+
+// 翻訳セクションのページを開いた際に、その言語での提出履歴を取得する。
+// 未ログインならisLoggedIn: falseを返し、コンポーネント側でログイン案内に切り替える
+export async function getTranslationHistory(
+ language: string,
+): Promise<{ isLoggedIn: boolean; submissions: TranslationSubmissionDTO[] }> {
+ const user = await getCurrentUser();
+ if (!user) {
+ return { isLoggedIn: false, submissions: [] };
+ }
+
+ const rows = await prisma.translationSubmission.findMany({
+ where: { userId: user.id, language },
+ orderBy: { createdAt: "desc" },
+ take: HISTORY_LIMIT,
+ });
+
+ return { isLoggedIn: true, submissions: rows.map(toTranslationSubmissionDTO) };
+}
+
+export interface SubmitTranslationInput {
+ language: string;
+ problemSource: TranslationProblemSource;
+ // problemSource: "preset" のとき使う(lib/translation/problems.tsのid)
+ problemId?: string;
+ // problemSource: "custom" のとき使う(設定画面で先に選ばせた級・訳す方向)
+ direction?: TranslationDirection;
+ level?: string;
+ // problemSource: "custom" のとき使う(ユーザーが自由に入力した原文)
+ customSourceText?: string;
+ content: string;
+ // 満点(TRANSLATION_SCORE_OPTIONSのいずれか)
+ scoreMax: number;
+}
+
+export type SubmitTranslationResult = { error: string } | { submission: TranslationSubmissionDTO };
+
+// 訳文を提出し、AI(OpenRouterの無料モデル)による添削結果を取得してDBに保存する
+export async function submitTranslation(
+ input: SubmitTranslationInput,
+): Promise {
+ const user = await requireUser();
+
+ const language = getLanguage(input.language);
+ if (!language) {
+ return { error: "言語が正しくありません。" };
+ }
+
+ if (!TRANSLATION_SCORE_OPTIONS.includes(input.scoreMax)) {
+ return { error: "満点の指定が正しくありません。" };
+ }
+
+ let problemId: string | null = null;
+ let direction: TranslationDirection;
+ let level: string;
+ let sourceText: string;
+ let referenceTranslation: string | null;
+
+ if (input.problemSource === "preset") {
+ const problem = input.problemId
+ ? getTranslationProblem(language.languageSlug, input.problemId)
+ : undefined;
+ if (!problem) {
+ return { error: "問題が見つかりませんでした。" };
+ }
+ problemId = problem.id;
+ direction = problem.direction;
+ level = problem.level;
+ sourceText = problem.sourceText;
+ referenceTranslation = problem.referenceTranslation;
+ } else if (input.problemSource === "custom") {
+ if (input.direction !== "fromJapanese" && input.direction !== "toJapanese") {
+ return { error: "訳す方向が正しくありません。" };
+ }
+ const levels = getTranslationLevels(language.languageSlug);
+ if (!input.level || !levels.includes(input.level)) {
+ return { error: "級が正しくありません。" };
+ }
+ const customSourceText = (input.customSourceText ?? "").trim();
+ if (!customSourceText) {
+ return { error: "原文を入力してください。" };
+ }
+ if (customSourceText.length > CUSTOM_SOURCE_MAX_LENGTH) {
+ return { error: `原文は${CUSTOM_SOURCE_MAX_LENGTH}字以内で入力してください。` };
+ }
+ direction = input.direction;
+ level = input.level;
+ sourceText = customSourceText;
+ referenceTranslation = null;
+ } else {
+ return { error: "題材の指定方法が正しくありません。" };
+ }
+
+ const content = input.content.trim();
+ if (!content) {
+ return { error: "訳文を入力してください。" };
+ }
+ if (content.length > CONTENT_MAX_LENGTH) {
+ return { error: `訳文は${CONTENT_MAX_LENGTH}字以内で入力してください。` };
+ }
+
+ let graded;
+ try {
+ graded = await gradeTranslation({
+ languageLabel: language.label,
+ direction,
+ level,
+ sourceText,
+ referenceTranslation,
+ content,
+ scoreMax: input.scoreMax,
+ });
+ } catch (error) {
+ if (error instanceof TranslationGradingError) {
+ return { error: error.message };
+ }
+ console.error("Translation grading failed", error);
+ return {
+ error: "AIによる添削中に予期しないエラーが発生しました。時間をおいて再度お試しください。",
+ };
+ }
+
+ const row = await prisma.translationSubmission.create({
+ data: {
+ userId: user.id,
+ language: language.languageSlug,
+ problemSource: input.problemSource,
+ direction,
+ problemId,
+ level,
+ sourceText,
+ referenceTranslation,
+ content,
+ feedback: graded.feedback as unknown as Prisma.InputJsonValue,
+ model: graded.model,
+ },
+ });
+
+ return { submission: toTranslationSubmissionDTO(row) };
+}
diff --git a/lib/translation/dto.ts b/lib/translation/dto.ts
new file mode 100644
index 0000000..e8e3aa7
--- /dev/null
+++ b/lib/translation/dto.ts
@@ -0,0 +1,40 @@
+import type {
+ TranslationDirection,
+ TranslationFeedback,
+ TranslationProblemSource,
+ TranslationSubmissionDTO,
+} from "./types";
+
+export interface TranslationSubmissionRow {
+ id: string;
+ language: string;
+ problemSource: string;
+ direction: string;
+ problemId: string | null;
+ level: string;
+ sourceText: string;
+ referenceTranslation: string | null;
+ content: string;
+ feedback: unknown;
+ model: string;
+ createdAt: Date;
+}
+
+export function toTranslationSubmissionDTO(
+ row: TranslationSubmissionRow,
+): TranslationSubmissionDTO {
+ return {
+ id: row.id,
+ language: row.language,
+ problemSource: row.problemSource as TranslationProblemSource,
+ direction: row.direction as TranslationDirection,
+ problemId: row.problemId,
+ level: row.level,
+ sourceText: row.sourceText,
+ referenceTranslation: row.referenceTranslation,
+ content: row.content,
+ feedback: row.feedback as TranslationFeedback,
+ model: row.model,
+ createdAt: row.createdAt.toISOString(),
+ };
+}
diff --git a/lib/translation/grading.ts b/lib/translation/grading.ts
new file mode 100644
index 0000000..d953eed
--- /dev/null
+++ b/lib/translation/grading.ts
@@ -0,0 +1,147 @@
+import "server-only";
+
+import type { TranslationDirection, TranslationFeedback } from "./types";
+
+// OpenRouterの無料枠モデル。無料モデルは提供元の混雑で一時的に429(レート制限)になることがあるため、
+// 空き状況の良いモデルを選んでいる。より良い無料モデルが出た場合はOPENROUTER_MODEL環境変数で上書きできる
+// (作文セクション lib/essay/grading.ts と同じモデル・同じ環境変数を利用する)
+const DEFAULT_MODEL = "nvidia/nemotron-3-super-120b-a12b:free";
+const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
+
+export class TranslationGradingError extends Error {}
+
+interface GradeTranslationInput {
+ languageLabel: string;
+ direction: TranslationDirection;
+ level: string;
+ sourceText: string;
+ // カスタム題材のときはnull(模範解答が存在しないため)
+ referenceTranslation: string | null;
+ content: string;
+ // 満点(設定画面でユーザーが選んだ点数。TRANSLATION_SCORE_OPTIONSのいずれか)
+ scoreMax: number;
+}
+
+const SYSTEM_PROMPT = `あなたは外国語教育に精通した、励ましを大切にする語学教師です。
+日本語話者の学習者が書いた訳文を、訳の正確さ・語彙・文法・自然さを総合的に踏まえて指定された満点で採点し、日本語で講評してください。
+項目ごとに分けず、総合的な1つの点数として採点してください。
+模範解答が示されている場合、それはあくまで一例であり、意味が通り自然であれば模範解答と表現が異なっていても減点しないでください。
+模範解答が示されていない場合は、原文の意味に忠実で自然な訳になっているかを基準に採点してください。
+学習者のやる気を削がないよう、採点はやや甘めを心がけてください。
+出力は指定されたJSON形式のみとし、それ以外の文章(説明・前置き・コードブロックの記号など)は一切含めないでください。`;
+
+function buildUserPrompt(input: GradeTranslationInput): string {
+ const directionLabel =
+ input.direction === "fromJapanese"
+ ? `日本語 → ${input.languageLabel}`
+ : `${input.languageLabel} → 日本語`;
+
+ const referenceLine = input.referenceTranslation
+ ? `【模範解答(一例)】${input.referenceTranslation}\n`
+ : "";
+
+ return `【言語】${input.languageLabel}
+【出題級】${input.level}
+【訳す方向】${directionLabel}
+【原文】${input.sourceText}
+${referenceLine}【学習者の訳文】
+${input.content}
+
+上記の訳文を、訳の正確さ・語彙・文法・自然さを総合的に踏まえて0〜${input.scoreMax}点の整数で採点してください。
+
+次のJSON形式のみで出力してください(キー名・構造を変えないこと)。scoreは0〜${input.scoreMax}点の整数、commentは日本語で2〜4文の具体的な講評(良い点・改善点や文法/語彙の誤りの指摘など)です。
+
+{
+ "score": number,
+ "comment": string
+}`;
+}
+
+function extractJsonObject(text: string): unknown {
+ const start = text.indexOf("{");
+ const end = text.lastIndexOf("}");
+ if (start === -1 || end === -1 || end < start) {
+ throw new TranslationGradingError("AIの応答からJSONを読み取れませんでした。");
+ }
+ try {
+ return JSON.parse(text.slice(start, end + 1));
+ } catch {
+ throw new TranslationGradingError("AIの応答を解析できませんでした。");
+ }
+}
+
+function clampScore(value: unknown, max: number): number {
+ const num = typeof value === "number" && Number.isFinite(value) ? value : 0;
+ return Math.min(max, Math.max(0, Math.round(num)));
+}
+
+function readComment(value: unknown): string {
+ return typeof value === "string" ? value.trim() : "";
+}
+
+function toFeedback(raw: unknown, scoreMax: number): TranslationFeedback {
+ if (typeof raw !== "object" || raw === null) {
+ throw new TranslationGradingError("AIの応答の形式が想定と異なります。");
+ }
+ const record = raw as Record;
+
+ return {
+ max: scoreMax,
+ score: clampScore(record.score, scoreMax),
+ comment: readComment(record.comment),
+ };
+}
+
+// OpenRouter(無料モデル)に翻訳の採点を依頼する
+export async function gradeTranslation(
+ input: GradeTranslationInput,
+): Promise<{ feedback: TranslationFeedback; model: string }> {
+ const apiKey = process.env.OPENROUTER_API_KEY;
+ if (!apiKey) {
+ throw new TranslationGradingError(
+ "AI添削の設定が完了していません(OPENROUTER_API_KEYが未設定です)。管理者に連絡してください。",
+ );
+ }
+ const model = process.env.OPENROUTER_MODEL || DEFAULT_MODEL;
+
+ let response: Response;
+ try {
+ response = await fetch(OPENROUTER_URL, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ model,
+ messages: [
+ { role: "system", content: SYSTEM_PROMPT },
+ { role: "user", content: buildUserPrompt(input) },
+ ],
+ response_format: { type: "json_object" },
+ temperature: 0.3,
+ }),
+ });
+ } catch {
+ throw new TranslationGradingError(
+ "AIサーバーへの接続に失敗しました。時間をおいて再度お試しください。",
+ );
+ }
+
+ if (!response.ok) {
+ throw new TranslationGradingError(
+ `AIによる添削に失敗しました(エラーコード: ${response.status})。時間をおいて再度お試しください。`,
+ );
+ }
+
+ const data = (await response.json()) as {
+ choices?: { message?: { content?: string } }[];
+ };
+ const text = data.choices?.[0]?.message?.content;
+ if (!text) {
+ throw new TranslationGradingError("AIから添削結果を取得できませんでした。");
+ }
+
+ const feedback = toFeedback(extractJsonObject(text), input.scoreMax);
+ return { feedback, model };
+}
diff --git a/lib/translation/problems.ts b/lib/translation/problems.ts
new file mode 100644
index 0000000..b8ca186
--- /dev/null
+++ b/lib/translation/problems.ts
@@ -0,0 +1,1199 @@
+// 翻訳セクションの事前用意問題(言語ごと・級ごとに10問=方向ごとに5問)。
+// levelは、その言語の検定試験(中検・仏検・独検・西検)の級に語彙・文法レベルを合わせている。
+// ここに項目を追加していけば、そのまま各言語の翻訳ページの選択肢に反映される。
+
+import type { TranslationDirection } from "./types";
+
+export interface TranslationProblem {
+ id: string;
+ direction: TranslationDirection;
+ // 対応する検定試験の名称・級(例: "仏検5級")
+ level: string;
+ // 訳す対象の原文(fromJapaneseなら日本語、toJapaneseならその言語)
+ sourceText: string;
+ // 模範解答(AIによる採点の参考として使う)
+ referenceTranslation: string;
+}
+
+// 1つの級につき、外国語→日本語・日本語→外国語をそれぞれ5問ずつ持つ「種データ」。
+// [原文, 模範解答] のタプル配列で持つことで、大量の級×問題を見通しよく管理する。
+interface LevelSeed {
+ levelSlug: string;
+ level: string;
+ toJapanese: [string, string][];
+ fromJapanese: [string, string][];
+}
+
+function buildProblems(
+ toJapanesePrefix: string,
+ fromJapanesePrefix: string,
+ seeds: LevelSeed[],
+): TranslationProblem[] {
+ const problems: TranslationProblem[] = [];
+ for (const seed of seeds) {
+ seed.toJapanese.forEach(([sourceText, referenceTranslation], i) => {
+ problems.push({
+ id: `${toJapanesePrefix}-${seed.levelSlug}-${String(i + 1).padStart(2, "0")}`,
+ direction: "toJapanese",
+ level: seed.level,
+ sourceText,
+ referenceTranslation,
+ });
+ });
+ seed.fromJapanese.forEach(([sourceText, referenceTranslation], i) => {
+ problems.push({
+ id: `${fromJapanesePrefix}-${seed.levelSlug}-${String(i + 1).padStart(2, "0")}`,
+ direction: "fromJapanese",
+ level: seed.level,
+ sourceText,
+ referenceTranslation,
+ });
+ });
+ }
+ return problems;
+}
+
+// ------------------------------------------------------------------
+// 中国語(中検: 準4級・4級・3級・2級・準1級・1級)
+// ------------------------------------------------------------------
+
+const CHINESE_SEEDS: LevelSeed[] = [
+ {
+ levelSlug: "jun4kyu",
+ level: "中検準4級",
+ toJapanese: [
+ [
+ "你好,我姓田中,是日本人,今年二十岁。",
+ "こんにちは、私は田中といいます。日本人で、今年20歳です。",
+ ],
+ ["我有一个哥哥和一个姐姐。", "私には兄が一人と姉が一人います。"],
+ ["今天天气很好,我很喜欢春天。", "今日はいい天気です。私は春が好きです。"],
+ ["星期天,我和朋友们一起运动。", "日曜日に、私は友達とスポーツをします。"],
+ ["我妈妈是老师,我爸爸是医生。", "私の母は教師で、父は医者です。"],
+ ],
+ fromJapanese: [
+ ["私は学生です。毎日学校で中国語を勉強しています。", "我是学生。我每天在学校学习汉语。"],
+ ["これは私の犬です。名前はミロです。", "这是我的狗。它叫米洛。"],
+ ["私はコーヒーが好きですが、紅茶は好きではありません。", "我喜欢咖啡,但是不喜欢红茶。"],
+ ["今日は月曜日です。明日は火曜日です。", "今天是星期一。明天是星期二。"],
+ ["この本はとても面白いです。", "这本书很有意思。"],
+ ],
+ },
+ {
+ levelSlug: "4kyu",
+ level: "中検4級",
+ toJapanese: [
+ [
+ "昨天,我跟妈妈一起去市场买了很多蔬菜。",
+ "昨日、私は母と一緒に市場へ行き、たくさんの野菜を買いました。",
+ ],
+ ["这个周末,我们打算去美术馆参观。", "今週末、私たちは美術館を訪れる予定です。"],
+ ["外面下着大雨,所以我带了雨伞。", "外は大雨が降っているので、傘を持っていきました。"],
+ ["这件毛衣多少钱?我想买。", "このセーターはいくらですか?買いたいです。"],
+ ["我每天坐公共汽车去学校。", "私は毎日バスで学校に行きます。"],
+ ],
+ fromJapanese: [
+ ["先週、私は家族と一緒に海へ旅行しました。", "上个星期,我跟家人一起去海边旅行了。"],
+ ["明日は友達の誕生日パーティーに行くつもりです。", "明天我打算去朋友的生日聚会。"],
+ ["このレストランの料理はとてもおいしいです。", "这家餐厅的菜很好吃。"],
+ ["駅までどうやって行けばいいですか?", "去车站怎么走?"],
+ ["昨日は忙しかったので、あまり眠れませんでした。", "昨天很忙,所以没怎么睡觉。"],
+ ],
+ },
+ {
+ levelSlug: "3kyu",
+ level: "中検3級",
+ toJapanese: [
+ [
+ "小时候,我跟爷爷奶奶一起住在乡下。",
+ "私が小さかった頃、祖父母と一緒に田舎に住んでいました。",
+ ],
+ [
+ "这家餐厅比那家贵,但是菜做得更好吃。",
+ "このレストランはもう一方より高いですが、料理はよりおいしいです。",
+ ],
+ ["我认为旅行是学习语言最好的方法。", "旅行することは言語を学ぶ最良の方法だと私は思います。"],
+ ["我们在中国留学期间参观了好几个城市。", "私たちは中国留学中にいくつもの都市を訪れました。"],
+ ["会议开得很长,不过内容很有意义。", "会議は長かったですが、とても有意義でした。"],
+ ],
+ fromJapanese: [
+ ["子供の頃、私はよくこの公園で遊んでいました。", "小时候,我常常在这个公园里玩儿。"],
+ ["このかばんはあのかばんよりも軽いです。", "这个包比那个包轻。"],
+ [
+ "彼女は毎日中国語を練習しているので、とても上手に話します。",
+ "她每天练习汉语,所以说得很好。",
+ ],
+ [
+ "私たちは去年、北京で開催された展覧会に行きました。",
+ "去年,我们去参观了在北京举办的展览。",
+ ],
+ ["健康のために、毎日野菜を食べるべきです。", "为了健康,应该每天吃蔬菜。"],
+ ],
+ },
+ {
+ levelSlug: "2kyu",
+ level: "中検2級",
+ toJapanese: [
+ [
+ "为了保护环境,每个人都做出努力是很重要的。",
+ "環境を守るために、一人一人が努力することが重要です。",
+ ],
+ [
+ "多亏了新技术,人与人之间的交流变得比以前更方便了。",
+ "新しい技術のおかげで、人々の間のコミュニケーションは以前より便利になりました。",
+ ],
+ [
+ "年轻一代对社会问题越来越关心。",
+ "若い世代は社会問題にますます関心を持つようになっています。",
+ ],
+ [
+ "虽然改变习惯很难,但是只要有意志就是可能的。",
+ "習慣を変えるのは難しいですが、意志さえあれば可能です。",
+ ],
+ [
+ "不但物价上涨了,而且工资也没有增加,这让很多人感到不安。",
+ "物価が上がっただけでなく、給料も増えていないので、多くの人が不安を感じています。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "地球温暖化を止めるためには、皆が協力する必要があります。",
+ "为了阻止全球变暖,大家都需要合作。",
+ ],
+ [
+ "インターネットの普及によって、情報を得ることがとても簡単になりました。",
+ "由于互联网的普及,获取信息变得非常容易。",
+ ],
+ [
+ "この問題については、様々な意見があることを理解する必要があります。",
+ "关于这个问题,需要理解存在各种各样的意见。",
+ ],
+ ["彼が試験に合格したことは、努力の結果だと思います。", "我认为他考试通过是努力的结果。"],
+ [
+ "経済が発展するにつれて、人々の生活水準も向上しました。",
+ "随着经济的发展,人们的生活水平也提高了。",
+ ],
+ ],
+ },
+ {
+ levelSlug: "jun1kyu",
+ level: "中検準1級",
+ toJapanese: [
+ [
+ "她并没有被失败打倒,反而从中汲取了重新开始的力量。",
+ "彼女は失敗に打ちのめされるどころか、そこから再出発する力を得ました。",
+ ],
+ [
+ "这家公司成功地把危机转化为了自我革新的契机。",
+ "この企業は危機をうまく転機に変え、自らを作り変えることに成功しました。",
+ ],
+ [
+ "认为技术进步本身就能保证幸福,未免过于天真。",
+ "技術の進歩さえあれば幸福が保証されると考えるのは、あまりにも単純すぎるでしょう。",
+ ],
+ [
+ "这位作家以细腻的笔触描绘了人性中的挣扎。",
+ "その作家は繊細な筆致で、人間性の中の葛藤を描いています。",
+ ],
+ [
+ "表面上看似简单的谈判,实际上比预想的要复杂得多。",
+ "見かけ上は単純に見える交渉も、実際は予想以上に複雑でした。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "彼は逆境をものともせず、むしろそれをばねにして成功を収めました。",
+ "他没有被逆境击倒,反而把它当作跳板取得了成功。",
+ ],
+ [
+ "この小説は、一見単純に見えて、実は非常に奥深いテーマを扱っています。",
+ "这部小说乍看简单,实际上探讨的是非常深刻的主题。",
+ ],
+ [
+ "経済成長だけを追い求めることの弊害が、今日ますます明らかになっています。",
+ "只追求经济增长所带来的弊端,如今变得越来越明显。",
+ ],
+ [
+ "彼女の発言は一見矛盾しているようだが、よく考えれば筋が通っている。",
+ "她的话乍听矛盾,但仔细想想却很有道理。",
+ ],
+ [
+ "交渉が難航したのは、双方が譲歩を拒んだからにほかならない。",
+ "谈判之所以陷入僵局,无非是因为双方都拒绝让步。",
+ ],
+ ],
+ },
+ {
+ levelSlug: "1kyu",
+ level: "中検1級",
+ toJapanese: [
+ [
+ "认为全球化能让所有国家平等受益,未免太天真了。",
+ "グローバル化があらゆる国に等しく恩恵をもたらすと考えるのは、あまりにも素朴でしょう。",
+ ],
+ [
+ "面对这样的两难境地,能够保持冷静的人少之又少。",
+ "そのようなジレンマに直面して冷静さを保てる人はごくわずかです。",
+ ],
+ [
+ "只有经过时间的沉淀,人们才能真正衡量这一事件的深远影响。",
+ "時間の沈殿を経て初めて、人々はこの出来事の深遠な影響を本当に測ることができるのです。",
+ ],
+ [
+ "这位作者不满足于揭露不公,还深入挖掘了其根源。",
+ "この著者は不正を暴くだけでは満足せず、その根源までも深く掘り下げています。",
+ ],
+ [
+ "只要根本原因得不到解决,问题的表象就会反复出现。",
+ "根本的な原因が解決されない限り、問題の表れは繰り返し現れるでしょう。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "この危機が浮き彫りにしたのは、既存の制度がいかに脆弱であったかという事実にほかならない。",
+ "这场危机所暴露出来的,正是现有制度是多么脆弱这一事实。",
+ ],
+ [
+ "議論を尽くしたところで、双方の溝が埋まる見込みは薄いように思われる。",
+ "即使把争论进行到底,双方的分歧似乎也很难弥合。",
+ ],
+ [
+ "彼の作品が今なお読み継がれているのは、人間性への深い洞察ゆえである。",
+ "他的作品之所以至今仍被人阅读,是因为其中蕴含着对人性的深刻洞察。",
+ ],
+ [
+ "表面上は些細に見える決定が、後になって重大な結果を招くことは少なくない。",
+ "表面上看似微不足道的决定,日后招致重大后果的情况并不少见。",
+ ],
+ [
+ "危機に際して真価を問われるのは、平時にはめったに試されない資質である。",
+ "在危机中受到考验的,是那些在平时很少被检验的品质。",
+ ],
+ ],
+ },
+];
+
+// ------------------------------------------------------------------
+// フランス語(仏検: 5級・4級・3級・準2級・2級・準1級・1級)
+// ------------------------------------------------------------------
+
+const FRENCH_SEEDS: LevelSeed[] = [
+ {
+ levelSlug: "5kyu",
+ level: "仏検5級",
+ toJapanese: [
+ [
+ "Bonjour, je m'appelle Paul. Je suis étudiant et j'habite à Lyon.",
+ "こんにちは、私はポールといいます。学生で、リヨンに住んでいます。",
+ ],
+ [
+ "J'ai vingt ans. J'ai un frère et une sœur.",
+ "私は20歳です。兄弟が一人と姉妹が一人います。",
+ ],
+ ["Il fait beau aujourd'hui. J'aime le printemps.", "今日はいい天気です。私は春が好きです。"],
+ ["Le dimanche, je fais du sport avec mes amis.", "日曜日に、私は友達とスポーツをします。"],
+ ["Ma mère est professeure et mon père est médecin.", "私の母は教師で、父は医者です。"],
+ ],
+ fromJapanese: [
+ [
+ "私は毎朝7時に起きます。それから朝食を食べます。",
+ "Je me lève à sept heures tous les matins. Ensuite, je prends le petit-déjeuner.",
+ ],
+ ["これは私の犬です。名前はミロです。", "C'est mon chien. Il s'appelle Milo."],
+ [
+ "私はコーヒーが好きですが、紅茶は好きではありません。",
+ "J'aime le café, mais je n'aime pas le thé.",
+ ],
+ ["今日は月曜日です。明日は火曜日です。", "Aujourd'hui, c'est lundi. Demain, c'est mardi."],
+ ["この本はとても面白いです。", "Ce livre est très intéressant."],
+ ],
+ },
+ {
+ levelSlug: "4kyu",
+ level: "仏検4級",
+ toJapanese: [
+ [
+ "Hier, je suis allé au marché avec ma mère pour acheter des légumes.",
+ "昨日、私は母と一緒に野菜を買いに市場へ行きました。",
+ ],
+ [
+ "Ce week-end, nous allons visiter le musée du Louvre.",
+ "今週末、私たちはルーヴル美術館を訪れる予定です。",
+ ],
+ [
+ "Il pleut beaucoup, alors je prends un parapluie.",
+ "雨がたくさん降っているので、傘を持っていきます。",
+ ],
+ [
+ "Combien coûte ce pull ? Je voudrais l'acheter.",
+ "このセーターはいくらですか?買いたいです。",
+ ],
+ ["Je prends le bus pour aller à l'école tous les jours.", "私は毎日バスで学校に行きます。"],
+ ],
+ fromJapanese: [
+ [
+ "先週、私は家族と一緒に海へ旅行しました。",
+ "La semaine dernière, je suis parti(e) en voyage à la mer avec ma famille.",
+ ],
+ [
+ "明日は友達の誕生日パーティーに行くつもりです。",
+ "Demain, je vais aller à la fête d'anniversaire de mon ami(e).",
+ ],
+ ["このレストランの料理はとてもおいしいです。", "La cuisine de ce restaurant est très bonne."],
+ ["駅までどうやって行けばいいですか?", "Comment est-ce que je peux aller à la gare ?"],
+ [
+ "昨日は忙しかったので、あまり眠れませんでした。",
+ "Hier, j'étais occupé(e), donc je n'ai pas beaucoup dormi.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "3kyu",
+ level: "仏検3級",
+ toJapanese: [
+ [
+ "Quand j'étais petit, j'habitais à la campagne avec mes grands-parents.",
+ "私が小さかった頃、祖父母と一緒に田舎に住んでいました。",
+ ],
+ [
+ "Ce restaurant est plus cher que l'autre, mais la nourriture est meilleure.",
+ "このレストランはもう一方より高いですが、料理はよりおいしいです。",
+ ],
+ [
+ "Je pense que voyager est le meilleur moyen d'apprendre une langue.",
+ "旅行することは言語を学ぶ最良の方法だと私は思います。",
+ ],
+ [
+ "Nous avons visité plusieurs villes pendant notre séjour en France.",
+ "私たちはフランス滞在中にいくつもの都市を訪れました。",
+ ],
+ [
+ "La réunion a été longue, mais elle était très utile.",
+ "会議は長かったですが、とても有意義でした。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "子供の頃、私はよくこの公園で遊んでいました。",
+ "Quand j'étais enfant, je jouais souvent dans ce parc.",
+ ],
+ ["このかばんはあのかばんよりも軽いです。", "Ce sac est plus léger que celui-là."],
+ [
+ "彼女は毎日フランス語を練習しているので、とても上手に話します。",
+ "Elle pratique le français tous les jours, donc elle parle très bien.",
+ ],
+ [
+ "私たちは去年、パリで開催された展覧会に行きました。",
+ "L'année dernière, nous sommes allé(e)s à l'exposition qui a eu lieu à Paris.",
+ ],
+ [
+ "健康のために、毎日野菜を食べるべきです。",
+ "Pour la santé, il faut manger des légumes tous les jours.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "jun2kyu",
+ level: "仏検準2級",
+ toJapanese: [
+ [
+ "Il est important que chacun fasse des efforts pour protéger l'environnement.",
+ "一人一人が環境を守るために努力することが重要です。",
+ ],
+ [
+ "Si j'avais plus de temps, je voyagerais dans le monde entier.",
+ "もし私にもっと時間があれば、世界中を旅行するのですが。",
+ ],
+ [
+ "Grâce aux nouvelles technologies, la communication est devenue plus facile qu'avant.",
+ "新しい技術のおかげで、コミュニケーションは以前より簡単になりました。",
+ ],
+ [
+ "Les jeunes générations s'intéressent de plus en plus aux questions sociales.",
+ "若い世代は社会問題にますます関心を持っています。",
+ ],
+ [
+ "Bien qu'il soit difficile de changer ses habitudes, c'est possible avec de la volonté.",
+ "習慣を変えるのは難しいけれども、意志があれば可能です。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "地球温暖化を止めるためには、皆が協力する必要があります。",
+ "Pour arrêter le réchauffement climatique, il faut que tout le monde coopère.",
+ ],
+ [
+ "もし私が首相だったら、教育により多くの予算を使うでしょう。",
+ "Si j'étais le premier ministre, je consacrerais plus de budget à l'éducation.",
+ ],
+ [
+ "インターネットの普及によって、情報を得ることがとても簡単になりました。",
+ "Grâce à la diffusion d'Internet, il est devenu très facile d'obtenir des informations.",
+ ],
+ [
+ "この問題については、様々な意見があることを理解する必要があります。",
+ "Il faut comprendre qu'il existe diverses opinions sur cette question.",
+ ],
+ [
+ "彼が試験に合格したことは、努力の結果だと思います。",
+ "Je pense que le fait qu'il ait réussi l'examen est le résultat de ses efforts.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "2kyu",
+ level: "仏検2級",
+ toJapanese: [
+ [
+ "Si nous avions pris cette décision plus tôt, la situation aurait pu être différente.",
+ "もし私たちがもっと早くこの決断をしていたら、状況は違っていたかもしれません。",
+ ],
+ [
+ "Le livre dont je vous ai parlé hier traite des inégalités sociales contemporaines.",
+ "昨日お話しした本は、現代の社会的不平等について扱っています。",
+ ],
+ [
+ "Il n'est pas rare que les entreprises modifient leur stratégie face à la concurrence internationale.",
+ "企業が国際競争に直面して戦略を変更することは珍しくありません。",
+ ],
+ [
+ "Quoi qu'on en dise, cette réforme aura des conséquences importantes sur l'économie.",
+ "何と言われようとも、この改革は経済に重大な影響を及ぼすでしょう。",
+ ],
+ [
+ "C'est un sujet sur lequel les experts eux-mêmes ne s'accordent pas toujours.",
+ "これは専門家自身も必ずしも意見が一致しない話題です。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "政府がもっと早く対策を取っていれば、被害はもっと少なかったでしょう。",
+ "Si le gouvernement avait pris des mesures plus tôt, les dégâts auraient été moindres.",
+ ],
+ [
+ "彼女が昨夜話していた映画は、実話に基づいているそうです。",
+ "Il paraît que le film dont elle parlait hier soir est basé sur une histoire vraie.",
+ ],
+ [
+ "少子化は多くの先進国が直面している共通の課題です。",
+ "La baisse de la natalité est un défi commun auquel de nombreux pays développés sont confrontés.",
+ ],
+ [
+ "この決定がどんな結果を招くにせよ、私たちはそれを受け入れなければなりません。",
+ "Quel que soit le résultat que cette décision entraîne, nous devons l'accepter.",
+ ],
+ [
+ "専門家の間でさえ意見が分かれているのは、この問題が複雑だからです。",
+ "Si les avis divergent même parmi les experts, c'est parce que ce problème est complexe.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "jun1kyu",
+ level: "仏検準1級",
+ toJapanese: [
+ [
+ "Loin de se laisser abattre par l'échec, elle y a puisé la force de recommencer.",
+ "彼女は失敗に打ちのめされるどころか、そこから再挑戦する力を得ました。",
+ ],
+ [
+ "Cette entreprise a su tirer parti de la crise pour se réinventer entièrement.",
+ "この企業は危機をうまく利用して、自らを完全に作り変えることに成功しました。",
+ ],
+ [
+ "On aurait tort de croire que le progrès technique suffit à garantir le bonheur.",
+ "技術の進歩さえあれば幸福が保証されると考えるのは間違いでしょう。",
+ ],
+ [
+ "L'écrivain dépeint avec une finesse remarquable les tourments de l'âme humaine.",
+ "その作家は人間の魂の苦悩を、見事な繊細さで描いています。",
+ ],
+ [
+ "Malgré les apparences, cette négociation s'est révélée plus délicate que prévu.",
+ "見かけによらず、この交渉は予想以上に繊細なものであることが明らかになりました。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "彼は逆境をものともせず、むしろそれをばねにして成功を収めました。",
+ "Loin de se laisser décourager par l'adversité, il s'en est plutôt servi comme tremplin vers la réussite.",
+ ],
+ [
+ "この小説は、一見単純に見えて、実は非常に奥深いテーマを扱っています。",
+ "Ce roman, qui semble simple à première vue, traite en réalité d'un thème d'une grande profondeur.",
+ ],
+ [
+ "経済成長だけを追い求めることの弊害が、今日ますます明らかになっています。",
+ "Les méfaits de la course exclusive à la croissance économique deviennent aujourd'hui de plus en plus évidents.",
+ ],
+ [
+ "彼女の発言は一見矛盾しているようだが、よく考えれば筋が通っている。",
+ "Ses propos semblent contradictoires à première vue, mais ils sont cohérents si l'on y réfléchit bien.",
+ ],
+ [
+ "交渉が難航したのは、双方が譲歩を拒んだからにほかならない。",
+ "Si les négociations ont été laborieuses, c'est uniquement parce que les deux parties ont refusé de faire des concessions.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "1kyu",
+ level: "仏検1級",
+ toJapanese: [
+ [
+ "Il serait naïf de penser que la mondialisation profite uniformément à tous les pays.",
+ "グローバル化があらゆる国に一様に恩恵をもたらすと考えるのは、あまりにも素朴でしょう。",
+ ],
+ [
+ "Rares sont ceux qui, confrontés à un tel dilemme, parviennent à garder la tête froide.",
+ "そのようなジレンマに直面して冷静さを保てる人はほとんどいません。",
+ ],
+ [
+ "Ce n'est qu'avec le recul que l'on mesure véritablement la portée de cet événement.",
+ "この出来事の本当の意味は、時が経ってから初めて測れるものです。",
+ ],
+ [
+ "L'auteur ne se contente pas de dénoncer l'injustice ; il en explore les racines les plus profondes.",
+ "著者は不正を告発するだけでなく、その最も深い根源を掘り下げています。",
+ ],
+ [
+ "Tant que les causes profondes ne seront pas traitées, les symptômes continueront de resurgir.",
+ "根本的な原因が対処されない限り、その症状は繰り返し現れ続けるでしょう。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "この危機が浮き彫りにしたのは、既存の制度がいかに脆弱であったかという事実にほかならない。",
+ "Ce que cette crise a mis en lumière, c'est précisément à quel point les institutions existantes étaient fragiles.",
+ ],
+ [
+ "議論を尽くしたところで、双方の溝が埋まる見込みは薄いように思われる。",
+ "Même à force d'épuiser le débat, il semble peu probable que le fossé entre les deux parties se comble.",
+ ],
+ [
+ "彼の作品が今なお読み継がれているのは、人間性への深い洞察ゆえである。",
+ "Si son œuvre continue d'être lue aujourd'hui, c'est en raison de sa profonde perspicacité sur la nature humaine.",
+ ],
+ [
+ "表面上は些細に見える決定が、後になって重大な結果を招くことは少なくない。",
+ "Il n'est pas rare qu'une décision qui paraît anodine en surface entraîne, par la suite, des conséquences considérables.",
+ ],
+ [
+ "危機に際して真価を問われるのは、平時にはめったに試されない資質である。",
+ "Ce sont des qualités rarement mises à l'épreuve en temps normal qui sont testées en période de crise.",
+ ],
+ ],
+ },
+];
+
+// ------------------------------------------------------------------
+// ドイツ語(独検: 5級・4級・3級・準2級・2級・準1級・1級)
+// ------------------------------------------------------------------
+
+const GERMAN_SEEDS: LevelSeed[] = [
+ {
+ levelSlug: "5kyu",
+ level: "独検5級",
+ toJapanese: [
+ [
+ "Guten Tag, ich heiße Anna. Ich komme aus Berlin und bin Studentin.",
+ "こんにちは、私はアンナといいます。ベルリン出身で、学生です。",
+ ],
+ [
+ "Ich bin zwanzig Jahre alt. Ich habe einen Bruder und eine Schwester.",
+ "私は20歳です。兄弟が一人と姉妹が一人います。",
+ ],
+ [
+ "Heute ist das Wetter schön. Ich mag den Frühling.",
+ "今日はいい天気です。私は春が好きです。",
+ ],
+ ["Am Sonntag mache ich Sport mit meinen Freunden.", "日曜日に、私は友達とスポーツをします。"],
+ ["Meine Mutter ist Lehrerin und mein Vater ist Arzt.", "私の母は教師で、父は医者です。"],
+ ],
+ fromJapanese: [
+ [
+ "私は週末に友達と映画を見ます。とても楽しいです。",
+ "Am Wochenende sehe ich mit meinen Freunden einen Film. Das macht viel Spaß.",
+ ],
+ ["これは私の犬です。名前はミロです。", "Das ist mein Hund. Er heißt Milo."],
+ [
+ "私はコーヒーが好きですが、紅茶は好きではありません。",
+ "Ich mag Kaffee, aber ich mag keinen Tee.",
+ ],
+ ["今日は月曜日です。明日は火曜日です。", "Heute ist Montag. Morgen ist Dienstag."],
+ ["この本はとても面白いです。", "Dieses Buch ist sehr interessant."],
+ ],
+ },
+ {
+ levelSlug: "4kyu",
+ level: "独検4級",
+ toJapanese: [
+ [
+ "Gestern bin ich mit meiner Mutter auf den Markt gegangen, um Gemüse zu kaufen.",
+ "昨日、私は母と一緒に野菜を買いに市場へ行きました。",
+ ],
+ [
+ "Dieses Wochenende besuchen wir das Museum in der Stadt.",
+ "今週末、私たちは街の美術館を訪れる予定です。",
+ ],
+ [
+ "Es regnet stark, deshalb nehme ich einen Regenschirm mit.",
+ "雨がたくさん降っているので、傘を持っていきます。",
+ ],
+ [
+ "Wie viel kostet dieser Pullover? Ich möchte ihn kaufen.",
+ "このセーターはいくらですか?買いたいです。",
+ ],
+ ["Ich fahre jeden Tag mit dem Bus zur Schule.", "私は毎日バスで学校に行きます。"],
+ ],
+ fromJapanese: [
+ [
+ "先週、私は家族と一緒に海へ旅行しました。",
+ "Letzte Woche bin ich mit meiner Familie ans Meer gereist.",
+ ],
+ [
+ "明日は友達の誕生日パーティーに行くつもりです。",
+ "Morgen gehe ich zur Geburtstagsparty meines Freundes.",
+ ],
+ [
+ "このレストランの料理はとてもおいしいです。",
+ "Das Essen in diesem Restaurant ist sehr lecker.",
+ ],
+ ["駅までどうやって行けばいいですか?", "Wie komme ich zum Bahnhof?"],
+ [
+ "昨日は忙しかったので、あまり眠れませんでした。",
+ "Gestern war ich beschäftigt, deshalb habe ich nicht viel geschlafen.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "3kyu",
+ level: "独検3級",
+ toJapanese: [
+ [
+ "Als ich klein war, wohnte ich mit meinen Großeltern auf dem Land.",
+ "私が小さかった頃、祖父母と一緒に田舎に住んでいました。",
+ ],
+ [
+ "Dieses Restaurant ist teurer als das andere, aber das Essen schmeckt besser.",
+ "このレストランはもう一方より高いですが、料理はよりおいしいです。",
+ ],
+ [
+ "Ich denke, dass Reisen die beste Art ist, eine Sprache zu lernen.",
+ "旅行することは言語を学ぶ最良の方法だと私は思います。",
+ ],
+ [
+ "Wir haben während unseres Aufenthalts in Deutschland mehrere Städte besucht.",
+ "私たちはドイツ滞在中にいくつもの都市を訪れました。",
+ ],
+ [
+ "Die Besprechung war lang, aber sie war sehr nützlich.",
+ "会議は長かったですが、とても有意義でした。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "子供の頃、私はよくこの公園で遊んでいました。",
+ "Als ich ein Kind war, spielte ich oft in diesem Park.",
+ ],
+ ["このかばんはあのかばんよりも軽いです。", "Diese Tasche ist leichter als jene."],
+ [
+ "彼女は毎日ドイツ語を練習しているので、とても上手に話します。",
+ "Sie übt jeden Tag Deutsch, deshalb spricht sie sehr gut.",
+ ],
+ [
+ "私たちは去年、ベルリンで開催された展覧会に行きました。",
+ "Letztes Jahr sind wir zu der Ausstellung gegangen, die in Berlin stattfand.",
+ ],
+ [
+ "健康のために、毎日野菜を食べるべきです。",
+ "Für die Gesundheit sollte man jeden Tag Gemüse essen.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "jun2kyu",
+ level: "独検準2級",
+ toJapanese: [
+ [
+ "Es ist wichtig, dass sich jeder bemüht, die Umwelt zu schützen.",
+ "一人一人が環境を守るために努力することが重要です。",
+ ],
+ [
+ "Wenn ich mehr Zeit hätte, würde ich um die ganze Welt reisen.",
+ "もし私にもっと時間があれば、世界中を旅行するのですが。",
+ ],
+ [
+ "Dank der neuen Technologien ist die Kommunikation einfacher geworden als früher.",
+ "新しい技術のおかげで、コミュニケーションは以前より簡単になりました。",
+ ],
+ [
+ "Die jüngeren Generationen interessieren sich zunehmend für soziale Fragen.",
+ "若い世代は社会問題にますます関心を持っています。",
+ ],
+ [
+ "Obwohl es schwer ist, Gewohnheiten zu ändern, ist es mit Willenskraft möglich.",
+ "習慣を変えるのは難しいけれども、意志があれば可能です。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "地球温暖化を止めるためには、皆が協力する必要があります。",
+ "Um die globale Erwärmung zu stoppen, müssen alle zusammenarbeiten.",
+ ],
+ [
+ "もし私が首相だったら、教育により多くの予算を使うでしょう。",
+ "Wenn ich Premierminister wäre, würde ich mehr Budget für die Bildung ausgeben.",
+ ],
+ [
+ "インターネットの普及によって、情報を得ることがとても簡単になりました。",
+ "Durch die Verbreitung des Internets ist es sehr einfach geworden, Informationen zu bekommen.",
+ ],
+ [
+ "この問題については、様々な意見があることを理解する必要があります。",
+ "Man muss verstehen, dass es zu diesem Thema verschiedene Meinungen gibt.",
+ ],
+ [
+ "彼が試験に合格したことは、努力の結果だと思います。",
+ "Ich denke, dass die Tatsache, dass er die Prüfung bestanden hat, das Ergebnis seiner Anstrengungen ist.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "2kyu",
+ level: "独検2級",
+ toJapanese: [
+ [
+ "Wenn wir diese Entscheidung früher getroffen hätten, wäre die Lage vielleicht anders gewesen.",
+ "もし私たちがもっと早くこの決断をしていたら、状況は違っていたかもしれません。",
+ ],
+ [
+ "Das Buch, von dem ich Ihnen gestern erzählt habe, behandelt die soziale Ungleichheit unserer Zeit.",
+ "昨日お話しした本は、現代の社会的不平等について扱っています。",
+ ],
+ [
+ "Es ist nicht selten, dass Unternehmen angesichts der internationalen Konkurrenz ihre Strategie ändern.",
+ "企業が国際競争に直面して戦略を変更することは珍しくありません。",
+ ],
+ [
+ "Was man auch sagen mag, diese Reform wird bedeutende Auswirkungen auf die Wirtschaft haben.",
+ "何と言われようとも、この改革は経済に重大な影響を及ぼすでしょう。",
+ ],
+ [
+ "Das ist ein Thema, bei dem sich selbst die Experten nicht immer einig sind.",
+ "これは専門家自身も必ずしも意見が一致しない話題です。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "政府がもっと早く対策を取っていれば、被害はもっと少なかったでしょう。",
+ "Wenn die Regierung früher Maßnahmen ergriffen hätte, wären die Schäden geringer gewesen.",
+ ],
+ [
+ "彼女が昨夜話していた映画は、実話に基づいているそうです。",
+ "Der Film, von dem sie gestern Abend erzählt hat, soll auf einer wahren Geschichte beruhen.",
+ ],
+ [
+ "少子化は多くの先進国が直面している共通の課題です。",
+ "Der Geburtenrückgang ist eine gemeinsame Herausforderung, vor der viele entwickelte Länder stehen.",
+ ],
+ [
+ "この決定がどんな結果を招くにせよ、私たちはそれを受け入れなければなりません。",
+ "Welche Folgen diese Entscheidung auch immer mit sich bringt, wir müssen sie akzeptieren.",
+ ],
+ [
+ "専門家の間でさえ意見が分かれているのは、この問題が複雑だからです。",
+ "Dass die Meinungen sogar unter Experten auseinandergehen, liegt daran, dass dieses Problem komplex ist.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "jun1kyu",
+ level: "独検準1級",
+ toJapanese: [
+ [
+ "Statt sich vom Misserfolg entmutigen zu lassen, schöpfte sie daraus die Kraft für einen Neuanfang.",
+ "彼女は失敗に落胆するどころか、そこから再出発する力を得ました。",
+ ],
+ [
+ "Diesem Unternehmen ist es gelungen, die Krise zu nutzen, um sich völlig neu zu erfinden.",
+ "この企業は危機をうまく利用して、自らを完全に作り変えることに成功しました。",
+ ],
+ [
+ "Es wäre falsch zu glauben, dass technischer Fortschritt allein das Glück garantiert.",
+ "技術の進歩さえあれば幸福が保証されると考えるのは間違いでしょう。",
+ ],
+ [
+ "Der Schriftsteller schildert mit bemerkenswerter Feinheit die Qualen der menschlichen Seele.",
+ "その作家は人間の魂の苦悩を、見事な繊細さで描いています。",
+ ],
+ [
+ "Entgegen dem äußeren Anschein erwies sich diese Verhandlung als heikler als erwartet.",
+ "見かけによらず、この交渉は予想以上に繊細なものであることが明らかになりました。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "彼は逆境をものともせず、むしろそれをばねにして成功を収めました。",
+ "Anstatt sich von den Widrigkeiten entmutigen zu lassen, nutzte er sie vielmehr als Sprungbrett zum Erfolg.",
+ ],
+ [
+ "この小説は、一見単純に見えて、実は非常に奥深いテーマを扱っています。",
+ "Dieser Roman, der auf den ersten Blick einfach erscheint, behandelt in Wirklichkeit ein sehr tiefgründiges Thema.",
+ ],
+ [
+ "経済成長だけを追い求めることの弊害が、今日ますます明らかになっています。",
+ "Die Schäden, die das ausschließliche Streben nach Wirtschaftswachstum verursacht, werden heute immer deutlicher.",
+ ],
+ [
+ "彼女の発言は一見矛盾しているようだが、よく考えれば筋が通っている。",
+ "Ihre Aussagen scheinen auf den ersten Blick widersprüchlich, sind aber bei genauerem Nachdenken schlüssig.",
+ ],
+ [
+ "交渉が難航したのは、双方が譲歩を拒んだからにほかならない。",
+ "Dass sich die Verhandlungen als schwierig erwiesen, lag einzig und allein daran, dass beide Seiten sich weigerten, Zugeständnisse zu machen.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "1kyu",
+ level: "独検1級",
+ toJapanese: [
+ [
+ "Es wäre naiv zu glauben, dass die Globalisierung allen Ländern gleichermaßen zugutekommt.",
+ "グローバル化があらゆる国に一様に恩恵をもたらすと考えるのは、あまりにも素朴でしょう。",
+ ],
+ [
+ "Nur wenige schaffen es, angesichts eines solchen Dilemmas einen kühlen Kopf zu bewahren.",
+ "そのようなジレンマに直面して冷静さを保てる人はほとんどいません。",
+ ],
+ [
+ "Erst mit zeitlichem Abstand lässt sich die wahre Tragweite dieses Ereignisses ermessen.",
+ "この出来事の本当の意味は、時が経ってから初めて測れるものです。",
+ ],
+ [
+ "Der Autor begnügt sich nicht damit, das Unrecht anzuprangern; er ergründet auch dessen tiefste Wurzeln.",
+ "著者は不正を告発するだけでなく、その最も深い根源を掘り下げています。",
+ ],
+ [
+ "Solange die tieferen Ursachen nicht angegangen werden, werden die Symptome immer wieder auftreten.",
+ "根本的な原因が対処されない限り、その症状は繰り返し現れ続けるでしょう。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "この危機が浮き彫りにしたのは、既存の制度がいかに脆弱であったかという事実にほかならない。",
+ "Was diese Krise offengelegt hat, ist genau die Tatsache, wie zerbrechlich die bestehenden Institutionen waren.",
+ ],
+ [
+ "議論を尽くしたところで、双方の溝が埋まる見込みは薄いように思われる。",
+ "Selbst wenn man die Debatte bis zum Äußersten führt, scheint es unwahrscheinlich, dass sich die Kluft zwischen beiden Seiten schließen lässt.",
+ ],
+ [
+ "彼の作品が今なお読み継がれているのは、人間性への深い洞察ゆえである。",
+ "Dass sein Werk bis heute gelesen wird, liegt an seiner tiefen Einsicht in das Wesen des Menschen.",
+ ],
+ [
+ "表面上は些細に見える決定が、後になって重大な結果を招くことは少なくない。",
+ "Es kommt nicht selten vor, dass eine Entscheidung, die oberflächlich betrachtet unbedeutend erscheint, später schwerwiegende Folgen nach sich zieht.",
+ ],
+ [
+ "危機に際して真価を問われるのは、平時にはめったに試されない資質である。",
+ "In Krisenzeiten werden Eigenschaften auf die Probe gestellt, die in normalen Zeiten selten getestet werden.",
+ ],
+ ],
+ },
+];
+
+// ------------------------------------------------------------------
+// スペイン語(西検: 6級・5級・4級・3級・2級・1級)
+// ------------------------------------------------------------------
+
+const SPANISH_SEEDS: LevelSeed[] = [
+ {
+ levelSlug: "6kyu",
+ level: "西検6級",
+ toJapanese: [
+ [
+ "Hola, me llamo Carlos. Soy estudiante y vivo en Madrid.",
+ "こんにちは、私はカルロスといいます。学生で、マドリードに住んでいます。",
+ ],
+ [
+ "Tengo veinte años. Tengo un hermano y una hermana.",
+ "私は20歳です。兄弟が一人と姉妹が一人います。",
+ ],
+ ["Hoy hace buen tiempo. Me gusta la primavera.", "今日はいい天気です。私は春が好きです。"],
+ ["Los domingos hago deporte con mis amigos.", "日曜日に、私は友達とスポーツをします。"],
+ ["Mi madre es profesora y mi padre es médico.", "私の母は教師で、父は医者です。"],
+ ],
+ fromJapanese: [
+ [
+ "私は毎日公園を散歩します。犬と一緒に歩くのが好きです。",
+ "Todos los días paseo por el parque. Me gusta caminar con mi perro.",
+ ],
+ ["これは私の犬です。名前はミロです。", "Este es mi perro. Se llama Milo."],
+ [
+ "私はコーヒーが好きですが、紅茶は好きではありません。",
+ "Me gusta el café, pero no me gusta el té.",
+ ],
+ ["今日は月曜日です。明日は火曜日です。", "Hoy es lunes. Mañana es martes."],
+ ["この本はとても面白いです。", "Este libro es muy interesante."],
+ ],
+ },
+ {
+ levelSlug: "5kyu",
+ level: "西検5級",
+ toJapanese: [
+ [
+ "Ayer fui al mercado con mi madre para comprar verduras.",
+ "昨日、私は母と一緒に野菜を買いに市場へ行きました。",
+ ],
+ [
+ "Este fin de semana vamos a visitar el museo del centro.",
+ "今週末、私たちは中心街の美術館を訪れる予定です。",
+ ],
+ [
+ "Está lloviendo mucho, así que llevo un paraguas.",
+ "雨がたくさん降っているので、傘を持っていきます。",
+ ],
+ [
+ "¿Cuánto cuesta este suéter? Quiero comprarlo.",
+ "このセーターはいくらですか?買いたいです。",
+ ],
+ ["Voy a la escuela en autobús todos los días.", "私は毎日バスで学校に行きます。"],
+ ],
+ fromJapanese: [
+ [
+ "先週、私は家族と一緒に海へ旅行しました。",
+ "La semana pasada viajé a la playa con mi familia.",
+ ],
+ [
+ "明日は友達の誕生日パーティーに行くつもりです。",
+ "Mañana voy a ir a la fiesta de cumpleaños de mi amigo.",
+ ],
+ [
+ "このレストランの料理はとてもおいしいです。",
+ "La comida de este restaurante está muy buena.",
+ ],
+ ["駅までどうやって行けばいいですか?", "¿Cómo puedo llegar a la estación?"],
+ [
+ "昨日は忙しかったので、あまり眠れませんでした。",
+ "Ayer estuve ocupado/a, así que no dormí mucho.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "4kyu",
+ level: "西検4級",
+ toJapanese: [
+ [
+ "Cuando era pequeño, vivía en el campo con mis abuelos.",
+ "私が小さかった頃、祖父母と一緒に田舎に住んでいました。",
+ ],
+ [
+ "Este restaurante es más caro que el otro, pero la comida es mejor.",
+ "このレストランはもう一方より高いですが、料理はよりおいしいです。",
+ ],
+ [
+ "Creo que viajar es la mejor manera de aprender un idioma.",
+ "旅行することは言語を学ぶ最良の方法だと私は思います。",
+ ],
+ [
+ "Visitamos varias ciudades durante nuestra estancia en España.",
+ "私たちはスペイン滞在中にいくつもの都市を訪れました。",
+ ],
+ ["La reunión fue larga, pero fue muy útil.", "会議は長かったですが、とても有意義でした。"],
+ ],
+ fromJapanese: [
+ [
+ "子供の頃、私はよくこの公園で遊んでいました。",
+ "Cuando era niño/a, jugaba a menudo en este parque.",
+ ],
+ ["このかばんはあのかばんよりも軽いです。", "Esta bolsa es más ligera que aquella."],
+ [
+ "彼女は毎日スペイン語を練習しているので、とても上手に話します。",
+ "Ella practica español todos los días, por eso habla muy bien.",
+ ],
+ [
+ "私たちは去年、マドリードで開催された展覧会に行きました。",
+ "El año pasado fuimos a la exposición que se celebró en Madrid.",
+ ],
+ [
+ "健康のために、毎日野菜を食べるべきです。",
+ "Para la salud, hay que comer verduras todos los días.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "3kyu",
+ level: "西検3級",
+ toJapanese: [
+ [
+ "Es importante que cada uno haga un esfuerzo para proteger el medio ambiente.",
+ "一人一人が環境を守るために努力することが重要です。",
+ ],
+ [
+ "Si tuviera más tiempo, viajaría por todo el mundo.",
+ "もし私にもっと時間があれば、世界中を旅行するのですが。",
+ ],
+ [
+ "Gracias a las nuevas tecnologías, la comunicación se ha vuelto más fácil que antes.",
+ "新しい技術のおかげで、コミュニケーションは以前より簡単になりました。",
+ ],
+ [
+ "Las nuevas generaciones se interesan cada vez más por los problemas sociales.",
+ "若い世代は社会問題にますます関心を持っています。",
+ ],
+ [
+ "Aunque sea difícil cambiar de hábitos, es posible si hay voluntad.",
+ "習慣を変えるのは難しいけれども、意志があれば可能です。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "地球温暖化を止めるためには、皆が協力する必要があります。",
+ "Para detener el calentamiento global, es necesario que todos cooperen.",
+ ],
+ [
+ "もし私が首相だったら、教育により多くの予算を使うでしょう。",
+ "Si yo fuera el primer ministro, dedicaría más presupuesto a la educación.",
+ ],
+ [
+ "インターネットの普及によって、情報を得ることがとても簡単になりました。",
+ "Gracias a la difusión de Internet, obtener información se ha vuelto muy fácil.",
+ ],
+ [
+ "この問題については、様々な意見があることを理解する必要があります。",
+ "Hay que entender que existen diversas opiniones sobre este tema.",
+ ],
+ [
+ "彼が試験に合格したことは、努力の結果だと思います。",
+ "Creo que el hecho de que él haya aprobado el examen es el resultado de su esfuerzo.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "2kyu",
+ level: "西検2級",
+ toJapanese: [
+ [
+ "Si hubiéramos tomado esta decisión antes, la situación podría haber sido diferente.",
+ "もし私たちがもっと早くこの決断をしていたら、状況は違っていたかもしれません。",
+ ],
+ [
+ "El libro del que te hablé ayer trata sobre las desigualdades sociales contemporáneas.",
+ "昨日お話しした本は、現代の社会的不平等について扱っています。",
+ ],
+ [
+ "No es raro que las empresas modifiquen su estrategia frente a la competencia internacional.",
+ "企業が国際競争に直面して戦略を変更することは珍しくありません。",
+ ],
+ [
+ "Digan lo que digan, esta reforma tendrá consecuencias importantes para la economía.",
+ "何と言われようとも、この改革は経済に重大な影響を及ぼすでしょう。",
+ ],
+ [
+ "Es un tema sobre el cual ni siquiera los expertos se ponen de acuerdo siempre.",
+ "これは専門家自身も必ずしも意見が一致しない話題です。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "政府がもっと早く対策を取っていれば、被害はもっと少なかったでしょう。",
+ "Si el gobierno hubiera tomado medidas antes, los daños habrían sido menores.",
+ ],
+ [
+ "彼女が昨夜話していた映画は、実話に基づいているそうです。",
+ "Dicen que la película de la que hablaba ella anoche está basada en una historia real.",
+ ],
+ [
+ "少子化は多くの先進国が直面している共通の課題です。",
+ "La baja natalidad es un desafío común al que se enfrentan muchos países desarrollados.",
+ ],
+ [
+ "この決定がどんな結果を招くにせよ、私たちはそれを受け入れなければなりません。",
+ "Sea cual sea el resultado que traiga esta decisión, tenemos que aceptarlo.",
+ ],
+ [
+ "専門家の間でさえ意見が分かれているのは、この問題が複雑だからです。",
+ "Que las opiniones estén divididas incluso entre los expertos se debe a que este problema es complejo.",
+ ],
+ ],
+ },
+ {
+ levelSlug: "1kyu",
+ level: "西検1級",
+ toJapanese: [
+ [
+ "Sería ingenuo pensar que la globalización beneficia a todos los países por igual.",
+ "グローバル化があらゆる国に一様に恩恵をもたらすと考えるのは、あまりにも素朴でしょう。",
+ ],
+ [
+ "Son pocos los que, ante semejante dilema, logran mantener la cabeza fría.",
+ "そのようなジレンマに直面して冷静さを保てる人はほとんどいません。",
+ ],
+ [
+ "Solo con la distancia del tiempo se puede medir verdaderamente el alcance de este acontecimiento.",
+ "この出来事の本当の意味は、時が経ってから初めて測れるものです。",
+ ],
+ [
+ "El autor no se limita a denunciar la injusticia, sino que explora sus raíces más profundas.",
+ "著者は不正を告発するだけでなく、その最も深い根源を掘り下げています。",
+ ],
+ [
+ "Mientras no se aborden las causas profundas, los síntomas seguirán reapareciendo.",
+ "根本的な原因が対処されない限り、その症状は繰り返し現れ続けるでしょう。",
+ ],
+ ],
+ fromJapanese: [
+ [
+ "この危機が浮き彫りにしたのは、既存の制度がいかに脆弱であったかという事実にほかならない。",
+ "Lo que esta crisis ha puesto de manifiesto es precisamente lo frágiles que eran las instituciones existentes.",
+ ],
+ [
+ "議論を尽くしたところで、双方の溝が埋まる見込みは薄いように思われる。",
+ "Por más que se agote el debate, parece poco probable que se cierre la brecha entre ambas partes.",
+ ],
+ [
+ "彼の作品が今なお読み継がれているのは、人間性への深い洞察ゆえである。",
+ "Si su obra se sigue leyendo hoy en día, es por su profunda perspicacia sobre la naturaleza humana.",
+ ],
+ [
+ "表面上は些細に見える決定が、後になって重大な結果を招くことは少なくない。",
+ "No es raro que una decisión que parece insignificante en la superficie acabe teniendo consecuencias graves.",
+ ],
+ [
+ "危機に際して真価を問われるのは、平時にはめったに試されない資質である。",
+ "En tiempos de crisis se ponen a prueba cualidades que rara vez se examinan en tiempos normales.",
+ ],
+ ],
+ },
+];
+
+export const TRANSLATION_PROBLEMS: Record = {
+ chinese: buildProblems("cn-zh2ja", "cn-ja2zh", CHINESE_SEEDS),
+ french: buildProblems("fr-fr2ja", "fr-ja2fr", FRENCH_SEEDS),
+ german: buildProblems("de-de2ja", "de-ja2de", GERMAN_SEEDS),
+ spanish: buildProblems("es-es2ja", "es-ja2es", SPANISH_SEEDS),
+};
+
+export function getTranslationProblems(
+ language: string,
+ direction?: TranslationDirection,
+): TranslationProblem[] {
+ const problems = TRANSLATION_PROBLEMS[language] ?? [];
+ return direction ? problems.filter((problem) => problem.direction === direction) : problems;
+}
+
+export function getTranslationProblem(
+ language: string,
+ problemId: string,
+): TranslationProblem | undefined {
+ return getTranslationProblems(language).find((problem) => problem.id === problemId);
+}
+
+// その言語で選べる級の一覧(易しい順)。カスタム題材を選ぶ場合も含め、設定画面で最初に選ばせる級の選択肢に使う
+export function getTranslationLevels(language: string): string[] {
+ const problems = TRANSLATION_PROBLEMS[language] ?? [];
+ return Array.from(new Set(problems.map((problem) => problem.level)));
+}
diff --git a/lib/translation/types.ts b/lib/translation/types.ts
new file mode 100644
index 0000000..aee8902
--- /dev/null
+++ b/lib/translation/types.ts
@@ -0,0 +1,46 @@
+// 翻訳セクション(/learn/[language]/03)で共通して使う型・定数。
+// サーバー側(採点ロジック・Server Action)とクライアント側(TranslationPractice)の両方から参照する。
+
+// "fromJapanese": 和文を外国語に訳す(和文 → 外国語)
+// "toJapanese": 外国語を和文に訳す(外国語 → 和文)
+export type TranslationDirection = "fromJapanese" | "toJapanese";
+
+// "preset": lib/translation/problems.tsの事前用意問題、"custom": ユーザー自身が用意した原文
+export type TranslationProblemSource = "preset" | "custom";
+
+// 採点は項目分けせず、訳の正確さ・語彙・文法・自然さを総合的に踏まえた1つの点数で行う。
+// 満点は1〜10点の範囲で1点刻みでユーザーが設定画面で選べる
+export const TRANSLATION_SCORE_MIN = 1;
+export const TRANSLATION_SCORE_MAX = 10;
+export const TRANSLATION_SCORE_DEFAULT: number = 10;
+
+export const TRANSLATION_SCORE_OPTIONS: number[] = Array.from(
+ { length: TRANSLATION_SCORE_MAX - TRANSLATION_SCORE_MIN + 1 },
+ (_, i) => TRANSLATION_SCORE_MIN + i,
+);
+
+// 自分で用意する原文の文字数上限
+export const CUSTOM_SOURCE_MAX_LENGTH = 300;
+
+export interface TranslationFeedback {
+ max: number;
+ score: number;
+ comment: string;
+}
+
+export interface TranslationSubmissionDTO {
+ id: string;
+ language: string;
+ problemSource: TranslationProblemSource;
+ direction: TranslationDirection;
+ // プリセット問題のとき: lib/translation/problems.ts内での識別子。カスタム題材のときはnull
+ problemId: string | null;
+ level: string;
+ sourceText: string;
+ // プリセット問題の模範解答(一例)。カスタム題材のときはnull
+ referenceTranslation: string | null;
+ content: string;
+ feedback: TranslationFeedback;
+ model: string;
+ createdAt: string;
+}
diff --git a/prisma/migrations/20260917120000_add_translation_submission/migration.sql b/prisma/migrations/20260917120000_add_translation_submission/migration.sql
new file mode 100644
index 0000000..ee4236f
--- /dev/null
+++ b/prisma/migrations/20260917120000_add_translation_submission/migration.sql
@@ -0,0 +1,19 @@
+-- CreateTable
+CREATE TABLE "TranslationSubmission" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" TEXT NOT NULL,
+ "language" TEXT NOT NULL,
+ "direction" TEXT NOT NULL,
+ "problemId" TEXT NOT NULL,
+ "level" TEXT NOT NULL,
+ "sourceText" TEXT NOT NULL,
+ "referenceTranslation" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "feedback" JSONB NOT NULL,
+ "model" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT "TranslationSubmission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE INDEX "TranslationSubmission_userId_language_createdAt_idx" ON "TranslationSubmission"("userId", "language", "createdAt");
diff --git a/prisma/migrations/20260917130000_customize_translation_settings/migration.sql b/prisma/migrations/20260917130000_customize_translation_settings/migration.sql
new file mode 100644
index 0000000..49a1583
--- /dev/null
+++ b/prisma/migrations/20260917130000_customize_translation_settings/migration.sql
@@ -0,0 +1,24 @@
+-- RedefineTables
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_TranslationSubmission" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" TEXT NOT NULL,
+ "language" TEXT NOT NULL,
+ "problemSource" TEXT NOT NULL DEFAULT 'preset',
+ "direction" TEXT NOT NULL,
+ "problemId" TEXT,
+ "level" TEXT NOT NULL,
+ "sourceText" TEXT NOT NULL,
+ "referenceTranslation" TEXT,
+ "content" TEXT NOT NULL,
+ "feedback" JSONB NOT NULL,
+ "model" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT "TranslationSubmission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+INSERT INTO "new_TranslationSubmission" ("id", "userId", "language", "direction", "problemId", "level", "sourceText", "referenceTranslation", "content", "feedback", "model", "createdAt")
+SELECT "id", "userId", "language", "direction", "problemId", "level", "sourceText", "referenceTranslation", "content", "feedback", "model", "createdAt" FROM "TranslationSubmission";
+DROP TABLE "TranslationSubmission";
+ALTER TABLE "new_TranslationSubmission" RENAME TO "TranslationSubmission";
+CREATE INDEX "TranslationSubmission_userId_language_createdAt_idx" ON "TranslationSubmission"("userId", "language", "createdAt");
+PRAGMA foreign_keys=ON;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 7236fc3..2347bbf 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -51,20 +51,21 @@ model Card {
}
model User {
- id String @id
- name String
- email String
- emailVerified Boolean @default(false)
- username String?
- displayUsername String?
- image String?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
- sessions Session[]
- accounts Account[]
- notebooks Notebook[]
- sectionProgress SectionProgress[]
- essaySubmissions EssaySubmission[]
+ id String @id
+ name String
+ email String
+ emailVerified Boolean @default(false)
+ username String?
+ displayUsername String?
+ image String?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ sessions Session[]
+ accounts Account[]
+ notebooks Notebook[]
+ sectionProgress SectionProgress[]
+ essaySubmissions EssaySubmission[]
+ translationSubmissions TranslationSubmission[]
@@unique([email])
@@unique([username])
@@ -75,23 +76,23 @@ model User {
// 「動詞の活用が苦手」「冠詞の一覧と縮約形が苦手」のように、セクション単位で苦手管理・復習日の提案を行う
model SectionProgress {
- id String @id @default(cuid())
- userId String
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ id String @id @default(cuid())
+ userId String
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// 言語スラッグ(例: "french")
- language String
+ language String
// セクションのスラッグ(例: "01")。app/learn/[language]/[sectionSlug] に対応する
sectionSlug String
// クイズ形式のセクション(例: 動詞活用ドリル)でのみ増える。読み物セクションでは常に0のまま
- attemptCount Int @default(0)
- correctCount Int @default(0)
+ attemptCount Int @default(0)
+ correctCount Int @default(0)
// Leitner式の箱番号(0〜6)。「正解」または「復習済みにする」操作のたび+1、誤答で0に戻る
- box Int @default(0)
+ box Int @default(0)
// クイズでの誤答による自動タグ。読み物セクションでは(誤答が存在しないため)常にfalseのまま
- autoTagged Boolean @default(false)
+ autoTagged Boolean @default(false)
// ユーザーが手動で付けたタグ。読み物セクションはこちらのみで苦手管理する
- manuallyTagged Boolean @default(false)
+ manuallyTagged Boolean @default(false)
// 復習が必要かどうか(autoTagged・manuallyTaggedのいずれかがtrueならtrue)。一覧表示・絞り込みに使う
needsReview Boolean @default(false)
nextReviewAt DateTime @default(now())
@@ -105,16 +106,16 @@ model SectionProgress {
// 作文セクション(/learn/essay/[language])での1回分の提出・AI添削結果
model EssaySubmission {
- id String @id @default(cuid())
- userId String
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ id String @id @default(cuid())
+ userId String
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// 言語スラッグ(例: "french")
language String
// お題の種類。"preset"ならlib/essay/topics.tsの事前用意お題、"custom"ならユーザー自身が指定したお題
- topicSource String
+ topicSource String
// プリセットお題を選んだ場合の、lib/essay/topics.ts内での識別子。カスタムお題ではnull
- topicId String?
+ topicId String?
// 実際に使われたお題文(プリセットの複製、またはユーザーが入力したお題)。
// プリセット内容が将来変わっても過去の提出時点の文言を保てるよう複製して保持する
topicPrompt String
@@ -125,7 +126,40 @@ model EssaySubmission {
// { vocabulary: {max,score,comment}, grammar: {...}, structure: {...}, content: {...}, overallComment }
feedback Json
// 採点に使ったOpenRouterのモデルID(監査・不具合調査用)
- model String
+ model String
+
+ createdAt DateTime @default(now())
+
+ @@index([userId, language, createdAt])
+}
+
+// 翻訳セクション(/learn/[language]/02)での1回分の提出・AI添削結果
+model TranslationSubmission {
+ id String @id @default(cuid())
+ userId String
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ // 言語スラッグ(例: "french")
+ language String
+
+ // 題材の種類。"preset"ならlib/translation/problems.tsの事前用意問題、"custom"ならユーザー自身が用意した原文
+ problemSource String @default("preset")
+ // 訳す方向。"fromJapanese"(和文→外国語)または"toJapanese"(外国語→和文)
+ direction String
+ // プリセット問題を選んだ場合の、lib/translation/problems.ts内での識別子。カスタム題材ではnull
+ problemId String?
+ // 出題時点の検定級(例: "仏検5級")。プリセット内容が将来変わっても過去の提出時点の文言を保てるよう複製して保持する
+ level String
+ // 出題時点の原文(同上の理由で複製して保持する)。カスタム題材ではユーザーが入力した原文
+ sourceText String
+ // プリセット問題の模範解答(同上の理由で複製して保持する)。カスタム題材には模範解答が無いためnull
+ referenceTranslation String?
+ content String
+
+ // AIによる採点・添削結果。項目分けせず、総合点1つで採点する(作文セクションと異なる点)
+ // { max, score, comment }
+ feedback Json
+ // 採点に使ったOpenRouterのモデルID(監査・不具合調査用)
+ model String
createdAt DateTime @default(now())