diff --git a/.github/workflows/readme-sync.yml b/.github/workflows/readme-sync.yml
new file mode 100644
index 0000000..76580e8
--- /dev/null
+++ b/.github/workflows/readme-sync.yml
@@ -0,0 +1,42 @@
+name: Check README Translations
+
+on:
+ pull_request:
+ paths:
+ - "README*.md"
+ - "tools/check_readme_sync.py"
+ - "tools/tests/**"
+ - ".github/workflows/readme-sync.yml"
+
+ workflow_dispatch:
+
+
+permissions:
+ contents: read
+
+
+concurrency:
+ group: readme-sync-${{ github.ref }}
+ cancel-in-progress: true
+
+
+jobs:
+ readme-sync:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+
+ - name: Set up Python
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ with:
+ python-version: "3.12"
+
+ # Compares the translations against README.md on roadmap row counts,
+ # per-row status, and specification numbers. Prose is never compared.
+ - name: Check translations against README.md
+ run: python tools/check_readme_sync.py
+
+ - name: Test the checker itself
+ run: python -m unittest discover -s tools/tests -v
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6f69a6e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+# Python bytecode from tools/
+__pycache__/
+*.py[cod]
diff --git a/README_Fr.md b/README_Fr.md
index 6c8a3c9..76f848e 100644
--- a/README_Fr.md
+++ b/README_Fr.md
@@ -156,14 +156,14 @@ Nous nous engageons à maintenir et à adapter en continu les principaux écosys
| Écosystème pris en charge | Statut | Description / Date de sortie estimée | Documentation associée |
| :------------------------------------------------------------------------------------- | :-------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Utilisation de base des moteurs** | ✅ Terminé | Contrôle de mouvement de base et encapsulation des API | [Robstride](https://wiki.seeedstudio.com/cn/robstride_control/) |
-| **Publication en open source des nouvelles pièces structurelles 3D STEP et de la BOM** | ✅ Terminé | Fichiers STEP de toutes les pièces de la nouvelle version, nomenclature des pièces (BOM) et prix de référence pour tous les composants usinés | [BOM reBot Arm B601-RS](./hardware/reBot_B601_RS/readme.md) |
+| **Publication en open source des nouvelles pièces structurelles 3D STEP et de la BOM** | ✅ Terminé | Fichiers STEP de toutes les pièces de la nouvelle version, nomenclature des pièces (BOM) et prix de référence pour tous les composants usinés | [BOM reBot Arm B601-RS](./hardware/reBot_B601_RS/README.md) |
| **Prise en main (Getting Started)** | ✅ Terminé | Guide de démarrage rapide du B601-RS | [Getting Started with reBot Arm B601-RS](https://wiki.seeedstudio.com/rebot_b601_rs_getting_started/) |
| **Vidéo d’assemblage** | ✅ Terminé | Étapes d’assemblage ultra détaillées avec vidéo explicative | [Vidéo d’assemblage reBot Arm B601-RS](https://wiki.seeedstudio.com/rebot_b601_rs_getting_started/) |
| **ROS2 (Humble)** | ✅ Terminé | Contrôleur reBot Arm intégré à ROS2 avec prise en charge de la cinématique, de la planification de trajectoire, de la compensation de gravité et de MoveIt2 | [Guide d’intégration ROS2 du reBot Arm B601-DM](https://wiki.seeedstudio.com/rebot_arm_b601_rs_ros2_integration/) |
| **Intégration LeRobot** | ✅ Terminé | Adaptation au framework d’entraînement Hugging Face LeRobot | [Guide de démarrage avec LeRobot pour le reBot Arm](https://wiki.seeedstudio.com/rebot_arm_b601_rs_lerobot/) |
| **Intégration Pinocchio** | ✅ Terminé | Adaptation au framework Pinocchio permettant la cinématique directe, la cinématique inverse et la compensation de gravité du bras robotique | [Guide de démarrage Pinocchio pour reBot Arm B601-DM](https://wiki.seeedstudio.com/rebot_arm_b601_rs_pinocchio_meshcat/) et [Dépôt Github](https://github.com/vectorBH6/reBotArm_control_py) |
| **Intégration caméra de profondeur** | ✅ Terminé | Démonstration de préhension visuelle basée sur YOLO et une caméra de profondeur | [Guide de démarrage de la démonstration de préhension visuelle](https://wiki.seeedstudio.com/rebot_arm_b601_dm_grasping_demo/) |
-| **Simulation Isaac Sim** | ⏳ Prévu | Importation des modèles USD et activation de la téléopération simulée | [Dépôt Github](https://github.com/Seeed-Projects/reBot-Isaacsim) |
+| **Simulation Isaac Sim** | ✅ Terminé | Importation des modèles USD et activation de la téléopération simulée | [Dépôt Github](https://github.com/Seeed-Projects/reBot-Isaacsim) |
| **Mises à jour progressives des derniers algorithmes** | ⏳ Prévu | Les algorithmes couramment utilisés dans l’industrie seront progressivement intégrés | En cours |
| **Lancement d’une série de cours entièrement gratuits** | ⏳ Prévu | Des cours gratuits couvrant les principaux algorithmes seront progressivement publiés | En cours |
diff --git a/README_JP.md b/README_JP.md
index 7a6a3ef..3a83738 100644
--- a/README_JP.md
+++ b/README_JP.md
@@ -152,19 +152,21 @@
| :--- | :---: | :--- | :--- |
| **ROS2 (Humble), third_party 統合, URDF / rebotarm_bringup** | [@danieldoradotalaveron-rb](https://github.com/danieldoradotalaveron-rb) | 1. **パッシブ診断モニター** (`rebotarm_monitor_ros2`): `rqt_robot_monitor` 向け `/diagnostics` オーバーレイ、serial/CAN 対応アグリゲーター;
2. **安全な退避とシャットダウン**: 接続時に休止姿勢を取得し、シャットダウンまたは `/rebotarm/park` 時にゆっくり戻して急落を防止;
3. **重力補償 (スムーズ停止)**: 重力補償終了時に MIT ランプアウトを行い、pos/vel 引き継ぎ時の衝撃、急動作、不安定性を低減;
4. **IK/FK と安全対策を備えたゲームパッドテレオペ**: IK によるエンドエフェクタ制御、RViz でのロボット状態のライブ可視化 (シミュレーションのみでテスト);
5. **D405 eye-in-hand TF**: `rebotarm_bringup` の `end_link` 下に Xacro 設定を追加し、RViz 可視化と TF のみに使用 (driver/depth/intrinsics なし)。取付姿勢は launch ファイルで調整可能、ブラケットキャリブレーションは未完了。Teleop FK/IK はアームのみの `fixend_core` URDF を使用し、RSP/RViz には完全な xacro を使用。 | [rebotarm_monitor_ros2](https://github.com/danieldoradotalaveron-rb/rebotarm_monitor_ros2)、[reBotArmController_ROS2](https://github.com/danieldoradotalaveron-rb/reBotArmController_ROS2) |
-| Écosystème pris en charge | Statut | Description / Date de sortie estimée | Documentation associée |
-| :------------------------------------------------------------------------------------- | :-------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **Utilisation de base des moteurs** | ✅ Terminé | Contrôle de mouvement de base et encapsulation des API | [Robstride](https://wiki.seeedstudio.com/cn/robstride_control/) |
-| **Publication en open source des nouvelles pièces structurelles 3D STEP et de la BOM** | ✅ Terminé | Fichiers STEP de toutes les pièces de la nouvelle version, nomenclature des pièces (BOM) et prix de référence pour tous les composants usinés | [BOM reBot Arm B601-RS](./hardware/reBot_B601_RS/readme.md) |
-| **Prise en main (Getting Started)** | ✅ Terminé | Guide de démarrage rapide du B601-RS | [Getting Started with reBot Arm B601-RS](https://wiki.seeedstudio.com/rebot_b601_rs_getting_started/) |
-| **Vidéo d’assemblage** | ✅ Terminé | Étapes d’assemblage ultra détaillées avec vidéo explicative | [Vidéo d’assemblage reBot Arm B601-RS](https://wiki.seeedstudio.com/rebot_b601_rs_getting_started/) |
-| **ROS2 (Humble)** | ✅ Terminé | Contrôleur reBot Arm intégré à ROS2 avec prise en charge de la cinématique, de la planification de trajectoire, de la compensation de gravité et de MoveIt2 | [Guide d’intégration ROS2 du reBot Arm B601-DM](https://wiki.seeedstudio.com/rebot_arm_b601_rs_ros2_integration/) |
-| **Intégration LeRobot** | ✅ Terminé | Adaptation au framework d’entraînement Hugging Face LeRobot | [Guide de démarrage avec LeRobot pour le reBot Arm](https://wiki.seeedstudio.com/rebot_arm_b601_rs_lerobot/) |
-| **Intégration Pinocchio** | ✅ Terminé | Adaptation au framework Pinocchio permettant la cinématique directe, la cinématique inverse et la compensation de gravité du bras robotique | [Guide de démarrage Pinocchio pour reBot Arm B601-DM](https://wiki.seeedstudio.com/rebot_arm_b601_rs_pinocchio_meshcat/) et [Dépôt Github](https://github.com/vectorBH6/reBotArm_control_py) |
-| **Intégration caméra de profondeur** | ✅ Terminé | Démonstration de préhension visuelle basée sur YOLO et une caméra de profondeur | [Guide de démarrage de la démonstration de préhension visuelle](https://wiki.seeedstudio.com/rebot_arm_b601_dm_grasping_demo/) |
-| **Simulation Isaac Sim** | ⏳ Prévu | Importation des modèles USD et activation de la téléopération simulée | [Dépôt Github](https://github.com/Seeed-Projects/reBot-Isaacsim) |
-| **Mises à jour progressives des derniers algorithmes** | ⏳ Prévu | Les algorithmes couramment utilisés dans l’industrie seront progressivement intégrés | En cours |
-| **Lancement d’une série de cours entièrement gratuits** | ⏳ Prévu | Des cours gratuits couvrant les principaux algorithmes seront progressivement publiés | En cours |
+### reBot Arm B601 RS
+
+| 対応エコシステム | 状態 | 説明 / 予定公開日 | 関連ドキュメント |
+| :--- | :---: | :--- | :--- |
+| **モーター基本使用** | ✅ 完了 | 基本的なモーション制御と API ラッパー化 | [Robstride](https://wiki.seeedstudio.com/cn/robstride_control/) |
+| **新バージョン STEP 3D 構造部品および BOM のオープンソース化** | ✅ 完了 | 新バージョンの全パーツの STEP ファイル、部品 BOM、およびすべての機械加工部品の参考価格 | [reBot Arm B601-RS BOM](./hardware/reBot_B601_RS/README.md) |
+| **クイックスタート** | ✅ 完了 | B601-RS のクイックスタート | [Getting Started with reBot Arm B601-RS](https://wiki.seeedstudio.com/rebot_b601_rs_getting_started/) |
+| **組み立て動画** | ✅ 完了 | 超詳細な組み立て手順と動画 | [reBot Arm B601-RS Assembly Video](https://wiki.seeedstudio.com/rebot_b601_rs_getting_started/) |
+| **ROS2 (Humble)** | ✅ 完了 | 運動学、軌道計画、重力補償および MoveIt2 に対応した ROS2 統合 reBot Arm コントローラー | [reBot Arm B601-RS ROS2 Integration Guide](https://wiki.seeedstudio.com/rebot_arm_b601_rs_ros2_integration/) |
+| **LeRobot 統合** | ✅ 完了 | Hugging Face LeRobot トレーニングフレームワークへの対応 | [Getting Started with LeRobot-based reBot Arm](https://wiki.seeedstudio.com/rebot_arm_b601_rs_lerobot/) |
+| **Pinocchio 統合** | ✅ 完了 | Pinocchio フレームワークへの対応により、ロボットアームの順運動学/逆運動学および重力補償を実現 | [Getting Started with Pinocchio for reBot Arm B601-RS](https://wiki.seeedstudio.com/rebot_arm_b601_rs_pinocchio_meshcat/) と [Github repo](https://github.com/vectorBH6/reBotArm_control_py) |
+| **深度カメラ統合** | ✅ 完了 | YOLO と深度カメラに基づくビジュアル把持デモ | [Getting Started with Visual Grasping Demo](https://wiki.seeedstudio.com/rebot_arm_b601_dm_grasping_demo/) |
+| **Isaac Sim シミュレーション** | ✅ 完了 | USD モデルをインポートし、シミュレーションによる遠隔操作を実現 | [Github Repo](https://github.com/Seeed-Projects/reBot-Isaacsim) |
+| **最新アルゴリズムの段階的更新** | ⏳ 計画中 | 主流アルゴリズムを段階的に更新予定 | Ongoing |
+| **完全無料コースシリーズの提供** | ⏳ 計画中 | 主流アルゴリズムを段階的に更新予定 | Ongoing |
---
diff --git a/README_zh.md b/README_zh.md
index a6347d7..10618df 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -131,6 +131,7 @@ reBot机械臂RS版本在[矽递科技电商平台](https://detail.tmall.com/ite
| **Isaac Sim 仿真** | 🚧 进行中 | 导入USD模型并实现仿真遥操作 | [由于添加额外课程延迟: 2026.06.20]|
| **LeRobot 适配** | ✅ 完成 | 适配 Hugging Face LeRobot 训练框架 | [reBot Arm B601-DM Lerobot使用教程](https://wiki.seeedstudio.com/cn/rebot_arm_b601_dm_lerobot/)|
| **深度相机集成** | ✅ 完成 | 基于 YOLO 与深度相机的视觉夹取演示 | [reBot Arm B601-DM 视觉夹取 Demo](https://wiki.seeedstudio.com/cn/rebot_arm_b601_dm_grasping_demo/) |
+| **reSpeaker 语音集成** | ✅ 完成 | 加入 reSpeaker Flex 4 麦克风阵列,构建具备空间感知能力的语音驱动智能机械臂控制系统 | [reBot Arm B601-DM 语音控制](https://wiki.seeedstudio.com/cn/control_rebot_arm_using_voice_with_respeaker_flex/) |
| **逐步更新最新算法** | ⏳ 计划中 | 逐步更新主流算法 | 持续进行 |
| **推出系列完全免费课程** | ⏳ 计划中 | 逐步更新主流算法 | 持续进行 |
@@ -147,7 +148,7 @@ reBot机械臂RS版本在[矽递科技电商平台](https://detail.tmall.com/ite
| **LeRobot 适配** | ✅ 完成 | 适配 Hugging Face LeRobot 训练框架 | [reBot Arm B601-RS入门Lerobot](https://wiki.seeedstudio.com/cn/rebot_arm_b601_rs_lerobot/)|
| **Pinocchio 适配** | ✅ 完成 | 适配 Pinocchio框架、实现机械臂正逆运动学及动力学重力补偿功能 |[reBot Arm B601-DM 的 Pinocchio 与 MeshCat 入门指南](https://wiki.seeedstudio.com/cn/rebot_arm_b601_rs_pinocchio_meshcat/) 以及 [Github 控制代码](https://github.com/vectorBH6/reBotArm_control_py)|
| **深度相机集成** | ✅ 完成 | 基于 YOLO 与深度相机的视觉夹取演示 | [reBot Arm B601-RS 视觉夹取 Demo](https://wiki.seeedstudio.com/cn/rebot_arm_b601_rs_grasping_demo/) |
-| **Isaac Sim 仿真** | ⏳ 计划中 | 导入USD模型并实现仿真遥操作 | [源代码](https://github.com/Seeed-Projects/reBot-Isaacsim)|
+| **Isaac Sim 仿真** | ✅ 完成 | 导入USD模型并实现仿真遥操作 | [源代码](https://github.com/Seeed-Projects/reBot-Isaacsim)|
| **逐步更新最新算法** | ⏳ 计划中 | 逐步更新主流算法 | 持续进行 |
| **推出系列完全免费课程** | ⏳ 计划中 | 逐步更新主流算法 | 持续进行 |
@@ -163,7 +164,7 @@ reBot-DevArm 专为桌面级具身智能应用设计,兼顾了负载能力与
| :--- | :--- | :--- |
| **工作负载 (Payload)** | 1.5kg | **2.5kg** |
| **推荐工作空间** | 70%臂展工作空间 | 70%臂展工作空间 |
-| **最大臂展 (Reach)** | 650 mm | **754 mm** |
+| **最大臂展 (Reach)** | 767 mm | **754 mm** |
| **自重 (Weight)** | **约 4.5 kg** |约 6.7 kg |
| **重复定位精度** | < 0.2 mm | < 0.2 mm |
| **自由度 (DOF)** | 6 DOF + 1 夹爪|6 DOF + 1 夹爪|
diff --git a/tools/check_readme_sync.py b/tools/check_readme_sync.py
new file mode 100644
index 0000000..1207c50
--- /dev/null
+++ b/tools/check_readme_sync.py
@@ -0,0 +1,179 @@
+#!/usr/bin/env python3
+"""Check the translated READMEs against the English one.
+
+The translations drift: a row gets added to README.md, a status flips from
+planned to completed, a specification is corrected — and the other four files
+keep the old version until a reader notices. This checks the things that must
+not differ between translations and deliberately ignores the things that must:
+
+ * how many roadmap tables there are,
+ * how many rows each of those tables has,
+ * the status glyph (OK / in progress / planned) on each row,
+ * the numbers in the hardware specification table.
+
+Prose is never compared, so translators stay free to phrase things their own
+way. Run it from the repository root:
+
+ python tools/check_readme_sync.py
+
+Exits 0 when the files agree, 1 when they do not.
+"""
+from __future__ import annotations
+
+import argparse
+import os
+import re
+import sys
+
+SOURCE = "README.md"
+TRANSLATIONS = ["README_zh.md", "README_JP.md", "README_Fr.md", "README_es.md"]
+
+STATUS_GLYPHS = "\u2705\U0001F6A7\u23F3" # done / in progress / planned
+STATUS_RE = re.compile(f"[{STATUS_GLYPHS}]")
+
+# A measurement in the specification table: "767 mm", "1.5kg", "DC 24V", "< 0.2 mm".
+# Decimal separators differ by locale (1.5 kg vs 1,5 kg), so they are normalised.
+NUMBER_RE = re.compile(r"\d+(?:[.,]\d+)?")
+
+
+def read_lines(path):
+ with open(path, encoding="utf-8") as handle:
+ return handle.read().splitlines()
+
+
+def split_tables(lines):
+ """Group consecutive pipe-table lines, keeping only tables with statuses."""
+ tables, current = [], []
+ for line in lines:
+ if line.strip().startswith("|"):
+ current.append(line)
+ elif current:
+ tables.append(current)
+ current = []
+ if current:
+ tables.append(current)
+ return [t for t in tables if any(STATUS_RE.search(line) for line in t)]
+
+
+def status_rows(table):
+ """The status glyphs of each row, in order."""
+ rows = []
+ for line in table:
+ if not STATUS_RE.search(line):
+ continue
+ rows.append("".join(STATUS_RE.findall(line)))
+ return rows
+
+
+def spec_numbers(lines):
+ """Numbers from the hardware specification table, row by row.
+
+ Identified by the row label mentioning a parameter the table always
+ carries; matching on position instead would break the moment a translation
+ reorders a row, which is allowed.
+ """
+ out = {}
+ for line in lines:
+ if not line.strip().startswith("|"):
+ continue
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
+ if len(cells) < 3:
+ continue
+ label = cells[0]
+ key = None
+ for marker, name in (
+ (r"Reach|臂展|リーチ|Portée|Alcance", "reach"),
+ (r"Payload|负载|負荷|Charge utile|Carga", "payload"),
+ (r"Weight|自重|Poids|Peso", "weight"),
+ (r"Voltage|电压|電圧|Tension|Tensión", "voltage"),
+ (r"Repeatability|重复定位|繰り返し|Répétabilité|Repetibilidad", "repeatability"),
+ ):
+ if re.search(marker, label, re.I):
+ key = name
+ break
+ if key is None or key in out:
+ continue
+ values = []
+ for cell in cells[1:3]:
+ values.append([n.replace(",", ".") for n in NUMBER_RE.findall(cell)])
+ out[key] = values
+ return out
+
+
+def check(root):
+ source_lines = read_lines(os.path.join(root, SOURCE))
+ source_tables = split_tables(source_lines)
+ source_specs = spec_numbers(source_lines)
+ problems = []
+
+ for name in TRANSLATIONS:
+ path = os.path.join(root, name)
+ if not os.path.exists(path):
+ problems.append(f"{name}: file is missing")
+ continue
+ lines = read_lines(path)
+ tables = split_tables(lines)
+
+ if len(tables) != len(source_tables):
+ problems.append(
+ f"{name}: has {len(tables)} roadmap tables, {SOURCE} has "
+ f"{len(source_tables)}"
+ )
+
+ for index, (src, dst) in enumerate(zip(source_tables, tables), start=1):
+ src_rows, dst_rows = status_rows(src), status_rows(dst)
+ if len(src_rows) != len(dst_rows):
+ problems.append(
+ f"{name}: roadmap table {index} has {len(dst_rows)} rows, "
+ f"{SOURCE} has {len(src_rows)} — a row was added or removed "
+ f"in one and not the other"
+ )
+ continue
+ for row, (a, b) in enumerate(zip(src_rows, dst_rows), start=1):
+ if a != b:
+ problems.append(
+ f"{name}: roadmap table {index} row {row} is marked "
+ f"{b!r}, {SOURCE} marks it {a!r}"
+ )
+
+ specs = spec_numbers(lines)
+ for key, expected in source_specs.items():
+ if key not in specs:
+ problems.append(f"{name}: specification table has no {key} row")
+ continue
+ if specs[key] != expected:
+ problems.append(
+ f"{name}: specification {key} is {specs[key]}, "
+ f"{SOURCE} says {expected}"
+ )
+
+ return problems
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "root",
+ nargs="?",
+ default=".",
+ help="repository root (default: current directory)",
+ )
+ args = parser.parse_args()
+
+ problems = check(args.root)
+ if not problems:
+ print(f"{len(TRANSLATIONS)} translations agree with {SOURCE}.")
+ return 0
+
+ print(f"{len(problems)} disagreement(s) with {SOURCE}:\n")
+ for problem in problems:
+ print(f" {problem}")
+ print(
+ "\nUpdate the translation to match README.md, or update README.md if it "
+ "is the one that is wrong."
+ )
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/tools/tests/test_check_readme_sync.py b/tools/tests/test_check_readme_sync.py
new file mode 100644
index 0000000..303fec8
--- /dev/null
+++ b/tools/tests/test_check_readme_sync.py
@@ -0,0 +1,139 @@
+"""Tests for tools/check_readme_sync.py.
+
+Run from the repository root with:
+
+ python -m unittest discover -s tools/tests -v
+"""
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+
+import check_readme_sync as sync # noqa: E402
+
+
+SOURCE = """\
+### reBot Arm B601 DM
+
+| Ecosystem | Status | Notes |
+| :--- | :---: | :--- |
+| **Motors** | \u2705 Completed | basic control |
+| **ROS2** | \u2705 Completed | controller |
+| **Isaac Sim** | \u2705 Completed | USD models |
+| **Courses** | \u23F3 Planned | later |
+
+## Specifications
+
+| Parameter | B601-DM | B601-RS |
+| :--- | :--- | :--- |
+| **Payload** | 1.5kg | 2.5kg |
+| **Max Reach** | 767 mm | 754 mm |
+| **Supply Voltage** | DC 24V | DC 48V |
+"""
+
+# Same facts, different language and phrasing, comma decimal separator.
+GOOD_TRANSLATION = """\
+### reBot Arm B601 DM
+
+| \u00c9cosyst\u00e8me | Statut | Notes |
+| :--- | :---: | :--- |
+| **Moteurs** | \u2705 Termin\u00e9 | contr\u00f4le de base |
+| **ROS2** | \u2705 Termin\u00e9 | contr\u00f4leur |
+| **Simulation Isaac Sim** | \u2705 Termin\u00e9 | mod\u00e8les USD |
+| **Cours** | \u23F3 Pr\u00e9vu | plus tard |
+
+## Sp\u00e9cifications
+
+| Param\u00e8tre | B601-DM | B601-RS |
+| :--- | :--- | :--- |
+| **Charge utile** | 1,5 kg | 2,5 kg |
+| **Port\u00e9e maximale** | 767 mm | 754 mm |
+| **Tension d'alimentation** | DC 24V | DC 48V |
+"""
+
+
+class SyncCheckTest(unittest.TestCase):
+ def setUp(self):
+ self.root = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, self.root, ignore_errors=True)
+ self.write(sync.SOURCE, SOURCE)
+ for name in sync.TRANSLATIONS:
+ self.write(name, GOOD_TRANSLATION)
+
+ def write(self, name, text):
+ with open(os.path.join(self.root, name), "w", encoding="utf-8") as handle:
+ handle.write(text)
+
+ def test_translations_that_agree_pass(self):
+ self.assertEqual(sync.check(self.root), [])
+
+ def test_differing_decimal_separator_is_not_a_difference(self):
+ """1,5 kg and 1.5kg are the same payload written two ways."""
+ self.assertEqual(sync.check(self.root), [])
+
+ def test_stale_status_is_caught(self):
+ """The real bug: Isaac Sim shipped, a translation still says planned."""
+ stale = GOOD_TRANSLATION.replace(
+ "| **Simulation Isaac Sim** | \u2705 Termin\u00e9 |",
+ "| **Simulation Isaac Sim** | \u23F3 Pr\u00e9vu |",
+ )
+ self.write("README_Fr.md", stale)
+ problems = sync.check(self.root)
+ self.assertEqual(len(problems), 1)
+ self.assertIn("README_Fr.md", problems[0])
+ self.assertIn("row 3", problems[0])
+
+ def test_missing_row_is_caught(self):
+ """A feature row added to English but never to a translation."""
+ short = GOOD_TRANSLATION.replace(
+ "| **Simulation Isaac Sim** | \u2705 Termin\u00e9 | mod\u00e8les USD |\n", ""
+ )
+ self.write("README_zh.md", short)
+ problems = sync.check(self.root)
+ self.assertTrue(any("3 rows" in p and "README_zh.md" in p for p in problems))
+
+ def test_superseded_specification_is_caught(self):
+ """The real bug: README_zh kept 650 mm after English moved to 767 mm."""
+ old = GOOD_TRANSLATION.replace("| 767 mm |", "| 650 mm |")
+ self.write("README_zh.md", old)
+ problems = sync.check(self.root)
+ self.assertEqual(len(problems), 1)
+ self.assertIn("reach", problems[0])
+ self.assertIn("650", problems[0])
+
+ def test_missing_specification_row_is_caught(self):
+ without = GOOD_TRANSLATION.replace(
+ "| **Port\u00e9e maximale** | 767 mm | 754 mm |\n", ""
+ )
+ self.write("README_JP.md", without)
+ problems = sync.check(self.root)
+ self.assertTrue(any("no reach row" in p for p in problems))
+
+ def test_missing_file_is_reported_not_crashed_on(self):
+ os.remove(os.path.join(self.root, "README_es.md"))
+ problems = sync.check(self.root)
+ self.assertTrue(any("is missing" in p for p in problems))
+
+ def test_extra_table_is_caught(self):
+ extra = GOOD_TRANSLATION + (
+ "\n| Extra | Statut |\n| :--- | :---: |\n| **Bonus** | \u2705 Termin\u00e9 |\n"
+ )
+ self.write("README_es.md", extra)
+ problems = sync.check(self.root)
+ self.assertTrue(any("roadmap tables" in p for p in problems))
+
+
+class RepositoryTest(unittest.TestCase):
+ """The real files in this repository must agree."""
+
+ def test_repository_readmes_are_in_sync(self):
+ root = os.path.join(os.path.dirname(__file__), "..", "..")
+ problems = sync.check(root)
+ self.assertEqual(problems, [], "\n".join(problems))
+
+
+if __name__ == "__main__":
+ unittest.main()