diff --git a/AGENTS.md b/AGENTS.md
index 1ff8196..5713b68 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -14,6 +14,45 @@ This file defines agent workflow and points to project specifications. Implement
4. When adding a new subsystem or feature, add/update the relevant spec in `specs/`.
5. Keep AGENTS.md high-level; do not duplicate implementation details from specs.
+## Updating NASA Data Files
+
+Use the Justfile workflow; do not reconstruct the TAP URLs or deployment steps
+from memory.
+
+```bash
+# 1. Download both NASA VOTables.
+just download-data
+
+# 2. Convert both VOTables to Parquet and generate both metadata TOML files.
+just convert-raw-files
+
+# 3. Confirm all generated runtime files exist and are non-empty.
+just verify-data
+
+# 4. Upload Parquet and metadata TOML files through Ansible.
+just ansible-upload-data
+
+# 5. Restart the deployed application so it loads the new files.
+just ansible-deploy
+```
+
+If the VOTable files have already been downloaded, start with
+`just convert-raw-files`. Ansible commands require
+`infrastructure/ansible/.env` with `DROPLET_IP` configured.
+
+The expected source files are `data/stellarhosts.vot` and
+`data/exoplanets.vot`. Conversion generates and overwrites:
+
+- `data/stellarhosts.parquet`
+- `data/exoplanets.parquet`
+- `data/stellarhosts-metadata.toml`
+- `data/exoplanets-metadata.toml`
+
+`convert-raw-files` processes every `.vot` file in `data/`; remove temporary or
+old VOTables before running it. Upload only the generated Parquet/TOML files,
+then restart/deploy because the application loads them at startup. See
+`specs/data-management.md` and `DEPLOY.md` for technical details.
+
## Agent Rules
1. **Explicit Instruction Compliance:** Do not perform actions (file edits or command execution) without explicit user request.
2. **Confidence Threshold:** If below ~70% confidence about a request or outcome, stop and ask for clarification.
diff --git a/Cargo.lock b/Cargo.lock
index fd4222b..0a84161 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -532,16 +532,6 @@ dependencies = [
"serde",
]
-[[package]]
-name = "calendrical_calculations"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5abbd6eeda6885048d357edc66748eea6e0268e3dd11f326fff5bd248d779c26"
-dependencies = [
- "core_maths",
- "displaydoc",
-]
-
[[package]]
name = "camino"
version = "1.2.2"
@@ -841,15 +831,6 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
-[[package]]
-name = "core_maths"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
-dependencies = [
- "libm",
-]
-
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -1294,7 +1275,7 @@ version = "0.1.0"
[[package]]
name = "exoplanets-catalog"
-version = "0.3.9"
+version = "0.3.10"
dependencies = [
"axum",
"console_error_panic_hook",
@@ -1879,50 +1860,6 @@ dependencies = [
"cc",
]
-[[package]]
-name = "icu_calendar"
-version = "2.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2b2acc6263f494f1df50685b53ff8e57869e47d5c6fe39c23d518ae9a4f3e45"
-dependencies = [
- "calendrical_calculations",
- "displaydoc",
- "icu_calendar_data",
- "icu_locale",
- "icu_locale_core",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_calendar_data"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "118577bcf3a0fa7c6ac0a7d6e951814da84ee56b9b1f68fb4d8d10b08cefaf4d"
-
-[[package]]
-name = "icu_casemap"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "070f98b5b82798fcb93654bf96ed9f40064fc44c86f51a09ea711092cd5cc5be"
-dependencies = [
- "icu_casemap_data",
- "icu_collections",
- "icu_locale_core",
- "icu_properties",
- "icu_provider",
- "potential_utf",
- "writeable",
- "zerovec",
-]
-
-[[package]]
-name = "icu_casemap_data"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "846b0857ca091204be3c874bc93daaf89d4777e8d2d20b0d3ffe8f671d98014b"
-
[[package]]
name = "icu_collections"
version = "2.2.0"
@@ -1931,125 +1868,12 @@ checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
dependencies = [
"displaydoc",
"potential_utf",
- "serde",
"utf8_iter",
"yoke",
"zerofrom",
"zerovec",
]
-[[package]]
-name = "icu_datetime"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "989d56ea5bbc43ae2b4e0388874b002884eaf4ed3a76c84a6c8c5ad575e04d72"
-dependencies = [
- "displaydoc",
- "fixed_decimal",
- "icu_calendar",
- "icu_datetime_data",
- "icu_decimal",
- "icu_locale",
- "icu_locale_core",
- "icu_pattern",
- "icu_plurals",
- "icu_provider",
- "icu_time",
- "potential_utf",
- "tinystr",
- "writeable",
- "zerovec",
-]
-
-[[package]]
-name = "icu_datetime_data"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40d3cc1b690d9703202bc319692ac8a1f3a6390686f0930ff40542450fa34f0b"
-
-[[package]]
-name = "icu_decimal"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "288247df2e32aa776ac54fdd64de552149ac43cb840f2761811f0e8d09719dd4"
-dependencies = [
- "displaydoc",
- "fixed_decimal",
- "icu_decimal_data",
- "icu_locale",
- "icu_locale_core",
- "icu_pattern",
- "icu_plurals",
- "icu_provider",
- "writeable",
- "zerovec",
-]
-
-[[package]]
-name = "icu_decimal_data"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f14a5ca9e8af29eef62064f269078424283d90dbaffeac5225addf62aaabc22"
-
-[[package]]
-name = "icu_experimental"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a881116e620fd635f564fd9cb9bc36c256b9da2221df8b3f55643d8ef32140f"
-dependencies = [
- "displaydoc",
- "either",
- "fixed_decimal",
- "icu_casemap",
- "icu_collections",
- "icu_decimal",
- "icu_experimental_data",
- "icu_list",
- "icu_locale",
- "icu_locale_core",
- "icu_normalizer",
- "icu_pattern",
- "icu_plurals",
- "icu_properties",
- "icu_provider",
- "litemap",
- "num-bigint",
- "num-rational",
- "num-traits",
- "potential_utf",
- "smallvec",
- "tinystr",
- "writeable",
- "zerotrie",
- "zerovec",
-]
-
-[[package]]
-name = "icu_experimental_data"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f72090d4f08a2bc94565cb02de6d5b87939424e462d9927d73a34f6f8e5d1232"
-
-[[package]]
-name = "icu_list"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aeeaf517689324395bed4767f7c65504f5455942ed4c14ee54c2087ca00b816e"
-dependencies = [
- "icu_list_data",
- "icu_locale",
- "icu_provider",
- "regex-automata",
- "writeable",
- "zerovec",
-]
-
-[[package]]
-name = "icu_list_data"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed62dbf114db9a4163481ed071509c4cd52cbcef9cb85979eba08a95549d73f3"
-
[[package]]
name = "icu_locale"
version = "2.2.0"
@@ -2105,20 +1929,6 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
-[[package]]
-name = "icu_pattern"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c4c568054ffe735398a9f4c55aec37ad7c768844553cc0978f09cc9b933a1fb"
-dependencies = [
- "displaydoc",
- "either",
- "serde",
- "writeable",
- "yoke",
- "zerovec",
-]
-
[[package]]
name = "icu_plurals"
version = "2.2.0"
@@ -2148,7 +1958,6 @@ dependencies = [
"icu_locale_core",
"icu_properties_data",
"icu_provider",
- "serde",
"zerotrie",
"zerovec",
]
@@ -2176,29 +1985,6 @@ dependencies = [
"zerovec",
]
-[[package]]
-name = "icu_time"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec3af0c141da0a61d4f6970cd1d5f4b388b17ea22f8124f8f6049d3d5147586a"
-dependencies = [
- "calendrical_calculations",
- "displaydoc",
- "icu_calendar",
- "icu_locale_core",
- "icu_provider",
- "icu_time_data",
- "serde",
- "zerotrie",
- "zerovec",
-]
-
-[[package]]
-name = "icu_time_data"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f2f8aeca682d874a5247084aa4fb7d1cef9ba45d889c21209a8818dcaaa0ec9"
-
[[package]]
name = "id-arena"
version = "2.3.0"
@@ -2410,9 +2196,11 @@ dependencies = [
"codee",
"cookie",
"default-struct-builder",
+ "http",
"js-sys",
"lazy_static",
"leptos",
+ "leptos_axum",
"paste",
"send_wrapper",
"thiserror 2.0.18",
@@ -2498,18 +2286,14 @@ checksum = "efc04f323545e008d9b6fe696d733557846ee17024af65b86b16c44a5c724d7b"
dependencies = [
"codee",
"default-struct-builder",
- "icu_calendar",
- "icu_datetime",
- "icu_decimal",
- "icu_experimental",
- "icu_list",
"icu_locale",
- "icu_plurals",
+ "js-sys",
"leptos",
"leptos-use",
"leptos_i18n_macro",
"leptos_meta",
"serde",
+ "serde-wasm-bindgen",
"typed-builder",
"wasm-bindgen",
"writeable",
@@ -2916,16 +2700,6 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "num-bigint"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
-dependencies = [
- "num-integer",
- "num-traits",
-]
-
[[package]]
name = "num-conv"
version = "0.2.2"
@@ -2943,26 +2717,6 @@ dependencies = [
"syn",
]
-[[package]]
-name = "num-integer"
-version = "0.1.46"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
-dependencies = [
- "num-traits",
-]
-
-[[package]]
-name = "num-rational"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
-dependencies = [
- "num-bigint",
- "num-integer",
- "num-traits",
-]
-
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -4615,6 +4369,17 @@ dependencies = [
"serde_derive",
]
+[[package]]
+name = "serde-wasm-bindgen"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
+dependencies = [
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
[[package]]
name = "serde_core"
version = "1.0.228"
@@ -6409,9 +6174,6 @@ name = "writeable"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
-dependencies = [
- "either",
-]
[[package]]
name = "wyz"
diff --git a/Cargo.toml b/Cargo.toml
index 55a5185..2cd0ebe 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "exoplanets-catalog"
-version = "0.3.9"
+version = "0.3.10"
edition = "2024"
[workspace]
@@ -49,7 +49,11 @@ utoipa-swagger-ui = { version = "9.0", features = ["axum"], optional = true }
sqlparser = { version = "0.60", optional = true }
moka = { version = "0.12", features = ["future"] }
rmcp = { version = "1.6.0", features = ["server", "macros", "transport-streamable-http-server", "schemars"], optional = true }
-leptos_i18n = "0.6.2"
+leptos_i18n = { version = "0.6.2", default-features = false }
+
+[package.metadata.leptos-i18n]
+default = "en"
+locales = ["en", "zh-CN", "ja"]
# [build-dependencies]
# markdown = "1.0.0-alpha"
@@ -60,7 +64,7 @@ serde_json = { workspace = true }
rand = "0.9"
[features]
-hydrate = ["leptos/hydrate", "dep:web-sys"]
+hydrate = ["leptos/hydrate", "leptos_i18n/hydrate", "dep:web-sys"]
ssr = [
"dep:axum",
"dep:tokio",
@@ -70,6 +74,7 @@ ssr = [
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
+ "leptos_i18n/axum",
"dep:tracing",
"dep:tracing-subscriber",
# data processing
diff --git a/Justfile b/Justfile
index 5e79aff..c18fbd1 100644
--- a/Justfile
+++ b/Justfile
@@ -2,18 +2,22 @@
clippy:
cargo clippy --all-targets --all-features -- -D warnings
-# Data download commands
-download-stellarhosts:
- curl "https://exoplanetarchive.ipac.caltech.edu/TAP/sync?query=select+*+from+stellarhosts" -L --max-time 3000 > data/stellarhosts.vot
-
-download-exoplanets:
- curl "https://exoplanetarchive.ipac.caltech.edu/TAP/sync?query=select+*+from+ps" -L --max-time 3000 > data/exoplanets.vot
+# Download both NASA VOTable sources.
+download-data:
+ mkdir -p data
+ curl --fail --location --remove-on-error --max-time 3000 --output data/stellarhosts.vot "https://exoplanetarchive.ipac.caltech.edu/TAP/sync?query=select+*+from+stellarhosts&format=votable"
+ curl --fail --location --remove-on-error --max-time 3000 --output data/exoplanets.vot "https://exoplanetarchive.ipac.caltech.edu/TAP/sync?query=select+*+from+pscomppars&format=votable"
+# Convert VOTables to Parquet and generate matching metadata TOML files.
convert-raw-files:
cargo run -p exodata -- dev convert-raw-files --data-dir data
-stellarhosts-metadata:
- cargo run -p exodata -- dev view-metadata --path data/stellarhosts.vot
+# Confirm all runtime files exist and are non-empty.
+verify-data:
+ test -s data/stellarhosts.parquet
+ test -s data/exoplanets.parquet
+ test -s data/stellarhosts-metadata.toml
+ test -s data/exoplanets-metadata.toml
# =============================================================================
# Ansible Deployment Commands
@@ -44,7 +48,7 @@ ansible-deploy:
ansible-ssl:
cd {{ansible_dir}} && ansible-playbook {{ansible_args}} playbooks/ssl.yml
-# Upload parquet data files
+# Upload Parquet data and metadata TOML files
ansible-upload-data:
cd {{ansible_dir}} && ansible-playbook {{ansible_args}} playbooks/upload-data.yml
diff --git a/docs/i18n/ja/index.md b/docs/i18n/ja/index.md
new file mode 100644
index 0000000..a36c07e
--- /dev/null
+++ b/docs/i18n/ja/index.md
@@ -0,0 +1,9 @@
+# [精密な系外惑星データを使って推論する](#mcp-exoplanet-data)
+
+Exodata は、ホスト型 MCP サーバーを通じて、NASA Exoplanet Archive の精密なデータへの構造化アクセスを AI エージェントに提供します。
+
+エージェントは一般的なモデル知識に頼ったり大規模なテーブルを手作業で調べたりせず、カタログのスキーマを確認し、SQL クエリを組み立て、返されたレコードを基に推論できます。
+
+利用可能なテーブルと列を実行時に確認するため、回答をカタログの結果に基づかせることができます。正確な値が重要な比較、絞り込み、ランキング、追加質問に適しています。
+
+互換性のある MCP クライアントを接続すると、カタログの現在の構造化データに基づく質問ができます。
diff --git a/docs/i18n/zh-CN/index.md b/docs/i18n/zh-CN/index.md
new file mode 100644
index 0000000..4ce6f59
--- /dev/null
+++ b/docs/i18n/zh-CN/index.md
@@ -0,0 +1,9 @@
+# [基于精确的系外行星数据进行推理](#mcp-exoplanet-data)
+
+Exodata 通过托管的 MCP 服务器,为 AI 智能体提供对 NASA 系外行星档案精确数据的结构化访问。
+
+智能体可以检查目录模式、编写 SQL 查询并根据返回的记录进行推理,而不是依赖模型的一般知识或手动浏览大型数据表。
+
+由于智能体会在运行时发现可用的数据表和字段,其回答能够以目录查询结果为依据。这适合需要精确数值的比较、筛选、排名和后续问题。
+
+连接兼容的 MCP 客户端,即可提出由目录当前结构化数据支持的问题。
diff --git a/docs/index.md b/docs/index.md
index 550829b..b97cfe1 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,13 +1,15 @@
-# Use the catalog, not just the table
+# [Reason over precise exoplanet data](#mcp-exoplanet-data)
-Exodata represents NASA Exoplanet Archive tables as a practical set of access
-paths for people, scripts, and AI-enabled tools.
+Exodata gives AI agents structured access to precise NASA Exoplanet Archive
+data through a hosted MCP server.
-Large scientific tables are powerful, but they are hard to scan by hand. This
-site keeps the table view available while adding shareable routes, focused
-JSON/CSV exports, REST and SQL endpoints, a local CLI, and an MCP server for
-natural-language-assisted catalog queries.
+Agents can inspect the catalog schema, compose SQL queries, and reason over the
+returned records instead of relying on general model knowledge or manually
+scanning large tables.
-Start with the website when you want to browse. Use exports when you need a
-single object record. Use the API, SQL endpoint, CLI, or MCP server when you
-want the dataset inside code, notebooks, local tools, or an agent workflow.
+Because the agent discovers the available tables and columns at runtime, its
+answers can stay grounded in catalog results. This is useful for comparisons,
+filtered searches, rankings, and follow-up questions where exact values matter.
+
+Connect a compatible MCP client to ask questions backed by the catalog's
+current structured data.
diff --git a/locales/en.json b/locales/en.json
new file mode 100644
index 0000000..38c769a
--- /dev/null
+++ b/locales/en.json
@@ -0,0 +1,99 @@
+{
+ "nav": {
+ "overview": "Overview",
+ "stellar_hosts": "Stellar Hosts",
+ "exoplanets": "Exoplanets",
+ "insights": "Insights",
+ "docs": "Docs",
+ "api": "API",
+ "github_repository": "GitHub repository",
+ "open_menu": "Open navigation menu",
+ "close_menu": "Close navigation menu",
+ "mobile_navigation": "Mobile navigation",
+ "language": "Language"
+ },
+ "home": {
+ "title": "Exoplanets Catalog",
+ "description": "Explore confirmed exoplanets and their host stars with searchable tables, discovery stats, and server-rendered detail pages.",
+ "hero_title": "🌌 Exoplanet Archive",
+ "hero_subtitle": "Exploring the cosmos: A comprehensive catalog of confirmed exoplanets and their host stars",
+ "loading": "Loading catalog data...",
+ "connection_error": "Connection Error",
+ "error_loading": "Error loading data",
+ "stellar_systems": "Stellar Systems",
+ "host_stars_catalogued": "Host stars catalogued",
+ "exoplanets": "Exoplanets",
+ "distinct_planets": "Distinct planets in the catalog",
+ "average_temperature": "Avg Stellar Temp",
+ "mean_temperature": "Mean effective temperature",
+ "average_distance": "Avg Distance",
+ "mean_distance": "Mean system distance",
+ "live": "LIVE",
+ "planet_classifications": "Planet Classifications",
+ "planet_classifications_subtitle": "Distinct planets grouped by canonical radius",
+ "orbital_periods": "Orbital Periods",
+ "orbital_periods_subtitle": "Distinct planets grouped by canonical period",
+ "discovery_methods": "Discovery Methods",
+ "discovery_methods_subtitle": "Distinct planets grouped by canonical method",
+ "discovery_years": "Discovery Years",
+ "discovery_years_subtitle": "Distinct planets grouped by earliest discovery year",
+ "temperature_bands": "Planet Temperature Bands",
+ "temperature_bands_subtitle": "Distinct planets grouped by canonical equilibrium temperature",
+ "detection_sources": "Detection Sources",
+ "detection_sources_subtitle": "Distinct planets grouped by discovery facility"
+ },
+ "manual": {
+ "examples_title": "Open a real catalog record",
+ "examples_subtitle": "Stable routes make pages, exports, API docs, and tool setup easy to share.",
+ "stellar_profile": "Stellar host profile",
+ "stellar_profile_description": "Open the TRAPPIST-1 host-system detail page.",
+ "planet_profile": "Planet profile",
+ "planet_profile_description": "Open the Kepler-22 b exoplanet detail page.",
+ "host_json": "Host JSON",
+ "host_json_description": "Download the full host detail payload.",
+ "host_csv": "Host CSV",
+ "host_csv_description": "Download matching host source rows.",
+ "planet_json": "Planet JSON",
+ "planet_json_description": "Download the full planet detail payload.",
+ "planet_csv": "Planet CSV",
+ "planet_csv_description": "Download matching planet source rows.",
+ "rest_api": "REST API",
+ "rest_api_description": "Query tables, schemas, details, insights, and SQL endpoints.",
+ "mcp_server": "MCP server",
+ "mcp_server_description": "Connect an AI client to inspect schema and query the catalog.",
+ "cli": "CLI",
+ "cli_description": "Query live or local data from the terminal.",
+ "swagger": "Swagger UI",
+ "swagger_description": "Explore the OpenAPI schema interactively.",
+ "connect_title": "Connect an MCP client",
+ "hosted_endpoint": "The hosted endpoint is",
+ "read": "Read the",
+ "mcp_introduction": "MCP introduction",
+ "or": "or the",
+ "mcp_docs": "Exodata MCP docs",
+ "copy": "Copy",
+ "copied": "Copied",
+ "ask": "Ask",
+ "tool": "Tool",
+ "sql": "SQL",
+ "result": "Result",
+ "ask_example": "nearest known planet-hosting systems with distance and planet count",
+ "result_example": "structured rows ready for a table, notebook, or follow-up query",
+ "cli_docs": "CLI docs",
+ "short_mcp_docs": "MCP docs",
+ "codex_config": "Codex config fallback",
+ "opencode_config": "OpenCode config",
+ "inspector_note": "Open the Inspector UI, then connect to the hosted endpoint with Streamable HTTP transport.",
+ "interaction_label": "Example catalog tool interaction"
+ },
+ "language": {
+ "english": "English",
+ "chinese": "Simplified Chinese",
+ "japanese": "Japanese"
+ },
+ "footer": {
+ "version": "Version",
+ "updated": "Updated",
+ "developed_by": "Developed by imscraping.ninja"
+ }
+}
diff --git a/locales/ja.json b/locales/ja.json
new file mode 100644
index 0000000..6a9df47
--- /dev/null
+++ b/locales/ja.json
@@ -0,0 +1,99 @@
+{
+ "nav": {
+ "overview": "概要",
+ "stellar_hosts": "恒星ホスト",
+ "exoplanets": "系外惑星",
+ "insights": "データ分析",
+ "docs": "ドキュメント",
+ "api": "API",
+ "github_repository": "GitHub リポジトリ",
+ "open_menu": "ナビゲーションメニューを開く",
+ "close_menu": "ナビゲーションメニューを閉じる",
+ "mobile_navigation": "モバイルナビゲーション",
+ "language": "言語"
+ },
+ "home": {
+ "title": "系外惑星カタログ",
+ "description": "検索可能なテーブル、発見統計、サーバー描画の詳細ページから、確認済み系外惑星とその主星を探索できます。",
+ "hero_title": "🌌 系外惑星アーカイブ",
+ "hero_subtitle": "宇宙を探索する:確認済み系外惑星とその主星の精密なカタログ",
+ "loading": "カタログデータを読み込んでいます…",
+ "connection_error": "接続エラー",
+ "error_loading": "データの読み込みエラー",
+ "stellar_systems": "恒星系",
+ "host_stars_catalogued": "収録済みの主星",
+ "exoplanets": "系外惑星",
+ "distinct_planets": "カタログ内の個別の惑星",
+ "average_temperature": "恒星の平均温度",
+ "mean_temperature": "平均有効温度",
+ "average_distance": "平均距離",
+ "mean_distance": "恒星系の平均距離",
+ "live": "ライブ",
+ "planet_classifications": "惑星の分類",
+ "planet_classifications_subtitle": "標準半径で分類した個別の惑星",
+ "orbital_periods": "公転周期",
+ "orbital_periods_subtitle": "標準周期で分類した個別の惑星",
+ "discovery_methods": "発見方法",
+ "discovery_methods_subtitle": "標準的な発見方法で分類した個別の惑星",
+ "discovery_years": "発見年",
+ "discovery_years_subtitle": "最初の発見年で分類した個別の惑星",
+ "temperature_bands": "惑星の温度帯",
+ "temperature_bands_subtitle": "標準平衡温度で分類した個別の惑星",
+ "detection_sources": "検出元",
+ "detection_sources_subtitle": "発見施設で分類した個別の惑星"
+ },
+ "manual": {
+ "examples_title": "実際のカタログレコードを開く",
+ "examples_subtitle": "安定したルートにより、ページ、エクスポート、API 文書、ツール設定を簡単に共有できます。",
+ "stellar_profile": "恒星ホストのプロフィール",
+ "stellar_profile_description": "TRAPPIST-1 ホスト系の詳細ページを開きます。",
+ "planet_profile": "惑星のプロフィール",
+ "planet_profile_description": "Kepler-22 b 系外惑星の詳細ページを開きます。",
+ "host_json": "ホスト JSON",
+ "host_json_description": "ホスト詳細の完全なデータをダウンロードします。",
+ "host_csv": "ホスト CSV",
+ "host_csv_description": "一致するホストのソース行をダウンロードします。",
+ "planet_json": "惑星 JSON",
+ "planet_json_description": "惑星詳細の完全なデータをダウンロードします。",
+ "planet_csv": "惑星 CSV",
+ "planet_csv_description": "一致する惑星のソース行をダウンロードします。",
+ "rest_api": "REST API",
+ "rest_api_description": "テーブル、スキーマ、詳細、分析、SQL エンドポイントを照会します。",
+ "mcp_server": "MCP サーバー",
+ "mcp_server_description": "AI クライアントを接続してスキーマを確認し、カタログを照会します。",
+ "cli": "CLI",
+ "cli_description": "ターミナルからオンラインまたはローカルのデータを照会します。",
+ "swagger": "Swagger UI",
+ "swagger_description": "OpenAPI スキーマを対話的に確認します。",
+ "connect_title": "MCP クライアントを接続",
+ "hosted_endpoint": "ホスト済みエンドポイント:",
+ "read": "参照:",
+ "mcp_introduction": "MCP 入門",
+ "or": "または",
+ "mcp_docs": "Exodata MCP ドキュメント",
+ "copy": "コピー",
+ "copied": "コピー済み",
+ "ask": "質問",
+ "tool": "ツール",
+ "sql": "SQL",
+ "result": "結果",
+ "ask_example": "既知の惑星ホスト系を距離が近い順に、距離と惑星数を含めて表示",
+ "result_example": "表、ノートブック、追加クエリに使える構造化された行",
+ "cli_docs": "CLI ドキュメント",
+ "short_mcp_docs": "MCP ドキュメント",
+ "codex_config": "Codex 代替設定",
+ "opencode_config": "OpenCode 設定",
+ "inspector_note": "Inspector UI を開き、Streamable HTTP トランスポートでホスト済みエンドポイントに接続します。",
+ "interaction_label": "カタログツール操作の例"
+ },
+ "language": {
+ "english": "英語",
+ "chinese": "簡体字中国語",
+ "japanese": "日本語"
+ },
+ "footer": {
+ "version": "バージョン",
+ "updated": "更新日時",
+ "developed_by": "imscraping.ninja が開発"
+ }
+}
diff --git a/locales/zh-CN.json b/locales/zh-CN.json
new file mode 100644
index 0000000..af82583
--- /dev/null
+++ b/locales/zh-CN.json
@@ -0,0 +1,99 @@
+{
+ "nav": {
+ "overview": "概览",
+ "stellar_hosts": "恒星宿主",
+ "exoplanets": "系外行星",
+ "insights": "数据洞察",
+ "docs": "文档",
+ "api": "API",
+ "github_repository": "GitHub 代码仓库",
+ "open_menu": "打开导航菜单",
+ "close_menu": "关闭导航菜单",
+ "mobile_navigation": "移动端导航",
+ "language": "语言"
+ },
+ "home": {
+ "title": "系外行星目录",
+ "description": "通过可搜索的数据表、发现统计和服务端渲染的详情页,探索已确认的系外行星及其宿主恒星。",
+ "hero_title": "🌌 系外行星档案",
+ "hero_subtitle": "探索宇宙:已确认系外行星及其宿主恒星的精确目录",
+ "loading": "正在加载目录数据……",
+ "connection_error": "连接错误",
+ "error_loading": "加载数据时出错",
+ "stellar_systems": "恒星系统",
+ "host_stars_catalogued": "已收录的宿主恒星",
+ "exoplanets": "系外行星",
+ "distinct_planets": "目录中的不同系外行星",
+ "average_temperature": "恒星平均温度",
+ "mean_temperature": "平均有效温度",
+ "average_distance": "平均距离",
+ "mean_distance": "恒星系统平均距离",
+ "live": "实时",
+ "planet_classifications": "行星分类",
+ "planet_classifications_subtitle": "按标准半径分组的不同系外行星",
+ "orbital_periods": "公转周期",
+ "orbital_periods_subtitle": "按标准周期分组的不同系外行星",
+ "discovery_methods": "发现方法",
+ "discovery_methods_subtitle": "按标准发现方法分组的不同系外行星",
+ "discovery_years": "发现年份",
+ "discovery_years_subtitle": "按最早发现年份分组的不同系外行星",
+ "temperature_bands": "行星温度区间",
+ "temperature_bands_subtitle": "按标准平衡温度分组的不同系外行星",
+ "detection_sources": "探测来源",
+ "detection_sources_subtitle": "按发现设施分组的不同系外行星"
+ },
+ "manual": {
+ "examples_title": "打开真实的目录记录",
+ "examples_subtitle": "稳定的路由让页面、导出文件、API 文档和工具配置易于分享。",
+ "stellar_profile": "恒星宿主资料",
+ "stellar_profile_description": "打开 TRAPPIST-1 宿主系统详情页。",
+ "planet_profile": "行星资料",
+ "planet_profile_description": "打开 Kepler-22 b 系外行星详情页。",
+ "host_json": "宿主 JSON",
+ "host_json_description": "下载完整的宿主详情数据。",
+ "host_csv": "宿主 CSV",
+ "host_csv_description": "下载匹配的宿主源数据行。",
+ "planet_json": "行星 JSON",
+ "planet_json_description": "下载完整的行星详情数据。",
+ "planet_csv": "行星 CSV",
+ "planet_csv_description": "下载匹配的行星源数据行。",
+ "rest_api": "REST API",
+ "rest_api_description": "查询数据表、模式、详情、洞察和 SQL 端点。",
+ "mcp_server": "MCP 服务器",
+ "mcp_server_description": "连接 AI 客户端以检查模式并查询目录。",
+ "cli": "CLI",
+ "cli_description": "从终端查询在线或本地数据。",
+ "swagger": "Swagger UI",
+ "swagger_description": "以交互方式浏览 OpenAPI 模式。",
+ "connect_title": "连接 MCP 客户端",
+ "hosted_endpoint": "托管端点为",
+ "read": "阅读",
+ "mcp_introduction": "MCP 入门",
+ "or": "或",
+ "mcp_docs": "Exodata MCP 文档",
+ "copy": "复制",
+ "copied": "已复制",
+ "ask": "提问",
+ "tool": "工具",
+ "sql": "SQL",
+ "result": "结果",
+ "ask_example": "距离最近的已知行星宿主系统,以及各自的距离和行星数量",
+ "result_example": "可用于表格、笔记本或后续查询的结构化数据行",
+ "cli_docs": "CLI 文档",
+ "short_mcp_docs": "MCP 文档",
+ "codex_config": "Codex 备用配置",
+ "opencode_config": "OpenCode 配置",
+ "inspector_note": "打开 Inspector 界面,然后使用 Streamable HTTP 传输连接到托管端点。",
+ "interaction_label": "目录工具交互示例"
+ },
+ "language": {
+ "english": "英语",
+ "chinese": "简体中文",
+ "japanese": "日语"
+ },
+ "footer": {
+ "version": "版本",
+ "updated": "更新时间",
+ "developed_by": "由 imscraping.ninja 开发"
+ }
+}
diff --git a/specs/ctx.md b/specs/ctx.md
index 2ff3542..431c8c7 100644
--- a/specs/ctx.md
+++ b/specs/ctx.md
@@ -1,302 +1,74 @@
# Current Task Context
-## Website Content and Multilingual Layout Plan
-
-This is planning context for a future implementation session. Do not implement
-the website changes from this file until explicitly requested.
-
-Primary direction:
-
-1. First improve the English website content and information architecture.
-2. Then add Chinese and Japanese interface localization on top of the improved
- English text.
-
-The multilingual work should not start by translating weak or temporary English
-copy. English content becomes the source text, then localized interface strings
-follow.
-
-## English Content First
-
-### Homepage Manual
-
-Implemented on 2026-06-28.
-
-- Added a distinct rounded homepage manual section in
- `src/components/homepage_manual.rs`, rendered after the detailed overview
- statistics and reachable from the hero subtitle link.
-- Source prose now lives in `docs/index.md` and is rendered with the existing
- docs Markdown renderer.
-- The section includes stable catalog example links, hosted MCP setup snippets
- for Codex, Claude Code, OpenCode, and MCP Inspector, copy buttons, and a
- compact CLI/MCP interaction card.
-- `docs/mcp.md` now documents only the hosted MCP URL in the connection summary.
-
-### Docs and Public Copy Cleanup
-
-Before translation, tighten the English source copy:
-
-- Make homepage/manual text concise and stable.
-- Make docs index copy suitable as the English source for translation.
-- Keep API/MCP/CLI examples exact and technical.
-- Separate prose that should be translated from command examples, URLs, JSON
- keys, SQL, and dataset field names that should remain unchanged.
-
-### Blog and Changelog Ideas
-
-Do not add a blog route in the first content pass. Keep these as future content
-ideas:
-
-- story posts about why tables are not enough for exoplanet catalogs
-- agent/MCP usage stories
-- small dataset stories based on insights pages
-- human-readable changelog summaries for major project milestones
-- technical notes about VOTable to Parquet to web/API/CLI flow
-
-`CHANGELOG.md` should remain technical. A future public notes page can summarize
-selected changes in a more narrative format.
-
-### AI Test-Generation Research Idea
-
-Keep this as a research spike, not production automation:
-
-- Explore using `rig.rs` with a DeepSeek Flash model to generate small candidate
- tests for distinct stellar-system and planet-system features.
-- Generated tests must be reviewed manually before committing.
-- Do not wire model calls into CI in the first experiment.
-- Do not infer behavior from exact fixture numbers when those numbers are only a
- sampled snapshot of the live dataset.
-
-## Multilingual Interface Plan
-
-Goal: add Chinese and Japanese interface support after the English content pass,
-without turning the first iteration into a full content-management platform.
-
-### Target Languages
-
-- Default: English (`en`)
-- Add: Simplified Chinese (`zh-CN`)
-- Add: Japanese (`ja`)
-
-UI labels:
-
-| Locale | Switcher label | Native label |
-|---|---|---|
-| `en` | EN | English |
-| `zh-CN` | 中文 | 简体中文 |
-| `ja` | 日本語 | 日本語 |
-
-Traditional Chinese is out of scope unless explicitly added later.
-
-### URL Layout
-
-Use prefixed locale routes for localized website pages:
-
-- `/` remains the canonical English homepage.
-- `/zh-CN` renders the Chinese homepage.
-- `/ja` renders the Japanese homepage.
-- English routes remain valid without prefixes:
- - `/stellarhosts`
- - `/exoplanets`
- - `/insights`
- - `/docs`
- - detail routes
-- Localized interface routes use the locale prefix:
- - `/zh-CN/stellarhosts`
- - `/ja/stellarhosts`
- - `/zh-CN/exoplanets`
- - `/ja/exoplanets`
- - `/zh-CN/insights`
- - `/ja/insights`
-
-Do not move non-website utility surfaces under locale prefixes:
-
-- keep `/rest/...`
-- keep `/mcp`
-- keep `/swagger-ui`
-- keep `/sitemap-index.xml` and child sitemap routes
-- keep current JSON/CSV export URL behavior
-
-Rationale: prefixed routes are clear for users and SEO, while unprefixed English
-routes preserve existing URLs.
-
-### Locale Detection and Rendering
-
-- Derive the active locale from the first path segment.
-- If the first path segment is `zh-CN` or `ja`, render interface strings in that
- locale.
-- Otherwise render English.
-- Data rows, entity names, API paths, SQL, JSON keys, CSV headers, and NASA field
- keys remain unchanged.
-- Detail route identifiers remain original catalog names and are not translated.
-
-The routing layer should avoid duplicating page implementations. Prefer a shared
-locale context/provider that all components can read.
-
-### Language Picker Component
-
-Plan a dedicated language picker component rather than scattering language links
-through the navbar.
-
-Behavior:
-
-- Render compact options: `EN`, `中文`, `日本語`.
-- Show the active locale visually.
-- Map the current page to the same page in the selected locale where possible:
- - `/exoplanets` -> `/ja/exoplanets`
- - `/ja/exoplanets` -> `/zh-CN/exoplanets`
- - `/stellarhosts/TRAPPIST-1` -> `/zh-CN/stellarhosts/TRAPPIST-1`
-- Preserve query strings for table pages when switching languages.
-- Do not prefix external links or utility routes.
-
-Placement:
-
-- Desktop: navbar, compact control near the right side.
-- Mobile: inside the mobile menu, preferably as a segmented row near the bottom.
-
-Accessibility:
-
-- Use a clear accessible label such as `Language`.
-- Each option should expose the native language name, not only the compact label.
-- Keyboard focus and current-state styling should be visible.
-
-### Translation Technology
-
-Use a real i18n crate instead of hand-written string tables if it works cleanly
-with the current Leptos version, SSR, and hydration.
-
-Preferred candidate for the future implementation spike:
-
-- `leptos_i18n`
-
-Why:
-
-- It is designed for Leptos applications.
-- It supports compile-time translation resources and component-level usage.
-- It is a better long-term fit than ad hoc match statements once interface copy
- spans navbar, tables, details, insights, docs chrome, metadata, and aria labels.
-
-Spike requirements before committing to it:
-
-- Confirm compatibility with the repository's Leptos version.
-- Confirm SSR and hydrate builds both work.
-- Confirm translations can be selected from URL-derived locale state.
-- Confirm metadata/head strings can use localized values.
-- Confirm the generated code does not make simple UI text awkward to maintain.
-
-Fallback if `leptos_i18n` does not fit:
-
-- Use a small internal translation module with semantic keys for v1.
-- Keep the API shaped so it can be replaced by an i18n crate later.
-
-Do not translate directly by using English strings as lookup keys. Use semantic
-keys or generated typed accessors so English copy can be edited without breaking
-translations.
-
-### Translation Surface
-
-Translate interface chrome and project-authored prose first:
-
-- navbar labels, mobile menu labels, aria labels
-- language picker labels
-- homepage hero/manual text and stat labels
-- loading, error, empty, and not-found states
-- table page headings and pagination text
-- column selector controls
-- insight page headings, cards, empty states, and explanatory text
-- detail page section headings, provenance/download controls, and summary labels
-- docs registry titles/descriptions and short localized docs summaries
-
-Do not translate source data values in v1:
-
-- planet names, stellar host names, system names
-- discovery methods, facilities, references, aliases, and record values
-- raw column keys such as `pl_name`, `hostname`, `st_teff`, and `sy_dist`
-- SQL examples, API paths, CLI commands, JSON keys, CSV headers
-
-Scientific units stay unchanged in v1: `K`, `pc`, `R⊕`, `M⊕`, `R☉`.
-
-### Docs Content
-
-Use separate Markdown files only for prose-heavy translated docs.
-
-Suggested future layout:
-
-```text
-docs/
-├── about.md
-├── api.md
-├── cli.md
-├── mcp.md
-└── i18n/
- ├── zh-CN/
- │ ├── about.md
- │ ├── api.md
- │ ├── cli.md
- │ └── mcp.md
- └── ja/
- ├── about.md
- ├── api.md
- ├── cli.md
- └── mcp.md
-```
-
-For the first localized docs pass, short localized summaries are enough. They
-can link to full English technical docs until full translations are ready.
-
-### SEO and Metadata
-
-- Set `` from the active locale.
-- Canonical URLs should point to the active localized page.
-- Add `hreflang` alternates for English, Chinese, and Japanese equivalents.
-- Include localized static routes in the sitemap after route behavior is stable.
-- Do not add localized copies of every detail route to the sitemap in v1.
-- Keep existing English detail routes as the primary entity URLs.
-
-### Layout Requirements
-
-- Use compact labels in the language picker to avoid crowding the navbar.
-- Avoid fixed-width text containers for translated copy.
-- Check Chinese and Japanese line wrapping in:
- - homepage cards/manual section
- - table controls and pagination
- - column selector
- - insight cards
- - detail-page download buttons
-- Preserve readable density. The localized interface should not become a separate
- visual design.
-
-### Suggested Rollout
-
-1. Polish English homepage/manual/docs source copy.
-2. Add locale route parsing, locale context, language picker, and localized href
- generation while still rendering English strings.
-3. Integrate the chosen i18n approach and translate global navigation plus common
- states.
-4. Translate homepage manual, table controls, insight chrome, and detail chrome.
-5. Add localized docs summaries.
-6. Add localized metadata, `hreflang`, and sitemap entries.
-
-### Verification
-
-Unit tests:
-
-- locale parsing from path
-- localized href generation with query-string preservation
-- utility routes remain unprefixed
-- canonical and alternate URL generation
-- docs link rewriting with locale prefixes
-
-SSR/manual checks:
-
-- `/`, `/zh-CN`, `/ja`
-- `/zh-CN/stellarhosts`, `/ja/exoplanets`
-- localized detail route switching
-- table query state survives language switching
-- `/rest`, `/mcp`, `/swagger-ui`, and export URLs remain unprefixed
-
-Browser checks:
-
-- desktop navbar does not overflow
-- mobile language picker is usable
-- CN/JP text does not overlap in cards, table controls, pagination, and detail
- action buttons
+## Localization Status
+
+The English homepage content pass and the first localization pass are complete.
+The implementation uses `leptos_i18n` with semantic keys and compile-time JSON
+resources for:
+
+- English (`en`) — default, unprefixed routes
+- Simplified Chinese (`zh-CN`) — `/zh-CN/...`
+- Japanese (`ja`) — `/ja/...`
+
+Traditional Chinese is out of scope unless explicitly requested.
+
+## Completed
+
+- Added locale-prefixed aliases for website routes without duplicating page
+ implementations.
+- Added the desktop navbar and mobile-menu language switcher with current-locale
+ styling, accessible labels, and same-page switching.
+- Preserved the current path, query string, and fragment when switching locale.
+- Kept `/rest`, `/mcp`, `/swagger-ui`, sitemap routes, and JSON/CSV exports
+ unprefixed.
+- Translated global navigation, mobile controls, footer text, homepage metadata,
+ hero, loading/error states, statistics headings, and the complete homepage
+ manual.
+- Added Chinese and Japanese homepage Markdown under `docs/i18n/`.
+- Set `` from the URL locale and added homepage canonical and
+ `hreflang` metadata.
+- Added `/zh-CN` and `/ja` to the static sitemap; localized table/detail routes
+ are intentionally not advertised yet.
+- Added tests for locale parsing, localized URL generation, URL-state
+ preservation, utility/export exclusions, and localized sitemap entries.
+- Verified formatting, Clippy, workspace tests, and the combined SSR/WASM
+ `cargo leptos build`.
+
+## Stable Rules
+
+- The URL is the only locale source. Do not add cookie, browser-language, or
+ `Accept-Language` redirects.
+- Unprefixed routes always render English; do not introduce `/en/...` routes.
+- Never translate catalog values, entity names, route identifiers, NASA field
+ keys, SQL, commands, API paths, JSON/CSV keys, or scientific units.
+- Keep stable homepage fragments identical across locales:
+ `#mcp-exoplanet-data`, `#catalog-examples-title`, and `#mcp-setup-title`.
+- Use locale resources for short interface strings and separate Markdown files
+ for prose-heavy translated content.
+
+## Deferred Translation Work
+
+The following page-specific content still renders in English, even under locale
+prefixes:
+
+- table headings, filters, column selector, pagination, and table states
+- stellar-host and exoplanet detail-page chrome
+- insight overview/detail chrome and explanatory text
+- not-found and shared application error/empty states
+- technical docs content and docs registry metadata
+- non-homepage localized metadata, canonical URLs, and `hreflang`
+
+Do not add these partially translated routes to the sitemap. Add localized
+sitemap entries only as each page surface is translated and receives correct
+localized metadata.
+
+## Recommended Next Pass
+
+1. Translate shared table controls and states while leaving dataset values and
+ column keys unchanged.
+2. Ensure table navigation, pagination, filters, and entity links retain the
+ active locale prefix and query state.
+3. Translate detail and insight chrome.
+4. Add localized docs summaries, then full translated docs only when reviewed.
+5. Expand canonical, `hreflang`, and sitemap coverage alongside each completed
+ surface.
diff --git a/specs/web-frontend.md b/specs/web-frontend.md
index fedec30..0691f98 100644
--- a/specs/web-frontend.md
+++ b/specs/web-frontend.md
@@ -11,6 +11,19 @@ The frontend is a reactive single-page application (SPA) built with:
- **Hydration** - Client-side interactivity after SSR
- **WASM** - Client-side code compiled to WebAssembly
+## Localization
+
+The website uses `leptos_i18n` with compile-time JSON resources for English,
+Simplified Chinese, and Japanese. English is the default and remains
+unprefixed; `/zh-CN` and `/ja` select the other locales. Locale-prefixed aliases
+exist for website routes, while REST, MCP, Swagger, sitemap, and export URLs
+remain unprefixed.
+
+The active locale is derived only from the URL. The language switcher preserves
+the current path, query string, and fragment. In the first localization pass,
+global navigation, the footer, and the homepage are translated; table, detail,
+insight, and technical documentation content remains English.
+
## Architecture
```
diff --git a/src/app.rs b/src/app.rs
index 960a74d..5fd7698 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -14,6 +14,7 @@ use crate::components::{
use crate::error_template::{AppError, ErrorTemplate};
use crate::metadata::{METADATA_SCRIPT_ID, provide_app_metadata_store};
use crate::metadata_helpers::SITE_NAME;
+use crate::{i18n::*, locale::locale_from_path};
use leptos::error::Errors;
use leptos::hydration::HydrationScripts;
@@ -66,19 +67,21 @@ pub fn App() -> impl IntoView {
- // content for this welcome page
-
- // Navigation bar (must be inside Router to use location)
-
+
+ // content for this welcome page
+
+
+ // Navigation bar (must be inside Router to use location)
+
-
- }
- .into_view()
- }>
+
+ }
+ .into_view()
+ }>
::new()} ssr=SsrMode::Async/>
impl IntoView {
::new()} ssr=SsrMode::Async/>
::new()} ssr=SsrMode::Async/>
::new()} ssr=SsrMode::Async/>
-
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+ ::new()} ssr=SsrMode::Async/>
+
-
-
+
+
+
}
}
+
+#[component]
+fn LocaleSync() -> impl IntoView {
+ use leptos_router::hooks::use_location;
+
+ let location = use_location();
+ let i18n = use_i18n();
+ i18n.set_locale_untracked(locale_from_path(
+ &location.pathname.get_untracked(),
+ ));
+
+ Effect::new(move |_| {
+ i18n.set_locale(locale_from_path(&location.pathname.get()));
+ });
+}
diff --git a/src/components/footer.rs b/src/components/footer.rs
index eafda15..5222b7a 100644
--- a/src/components/footer.rs
+++ b/src/components/footer.rs
@@ -1,5 +1,7 @@
use leptos::prelude::*;
+use crate::i18n::*;
+
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
const BUILD_TIMESTAMP: &str = match option_env!("BUILD_TIMESTAMP") {
Some(value) => value,
@@ -8,15 +10,16 @@ const BUILD_TIMESTAMP: &str = match option_env!("BUILD_TIMESTAMP") {
#[component]
pub fn VersionFooter() -> impl IntoView {
+ let i18n = use_i18n();
view! {
diff --git a/src/components/homepage_manual.rs b/src/components/homepage_manual.rs
index 1ec06d5..41ffc79 100644
--- a/src/components/homepage_manual.rs
+++ b/src/components/homepage_manual.rs
@@ -1,20 +1,29 @@
use leptos::prelude::*;
use crate::components::docs::render::render_markdown;
+use crate::i18n::*;
+use crate::locale::localized_path;
-const MANUAL_MARKDOWN: &str = include_str!("../../docs/index.md");
-const HOST_NAME: &str = "TRAPPIST-1";
+const MANUAL_MARKDOWN_EN: &str = include_str!("../../docs/index.md");
+const MANUAL_MARKDOWN_ZH_CN: &str =
+ include_str!("../../docs/i18n/zh-CN/index.md");
+const MANUAL_MARKDOWN_JA: &str = include_str!("../../docs/i18n/ja/index.md");
const HOST_PATH: &str = "/stellarhosts/TRAPPIST-1";
-const PLANET_NAME: &str = "Kepler-22 b";
const PLANET_PATH: &str = "/exoplanets/Kepler-22%20b";
const MCP_ENDPOINT: &str = "https://exodata.space/mcp";
#[component]
pub fn HomepageManual() -> impl IntoView {
- let html = render_markdown(MANUAL_MARKDOWN);
+ let locale = use_i18n().get_locale_untracked();
+ let markdown = match locale {
+ Locale::en => MANUAL_MARKDOWN_EN,
+ Locale::zh_CN => MANUAL_MARKDOWN_ZH_CN,
+ Locale::ja => MANUAL_MARKDOWN_JA,
+ };
+ let html = render_markdown(markdown);
view! {
-
+
impl IntoView {
#[component]
fn ExampleLinks() -> impl IntoView {
+ let i18n = use_i18n();
+ let locale = i18n.get_locale_untracked();
view! {
-
"Open a real catalog record"
-
"Stable routes make pages, exports, API docs, and tool setup easy to share."
+
+
{t!(i18n, manual.examples_subtitle)}
@@ -99,9 +112,9 @@ fn ExampleLinks() -> impl IntoView {
#[component]
fn ExampleLink(
- href: &'static str,
+ href: String,
title: &'static str,
- description: String,
+ description: &'static str,
) -> impl IntoView {
view! {
@@ -113,23 +126,27 @@ fn ExampleLink(
#[component]
fn McpSetup() -> impl IntoView {
+ let i18n = use_i18n();
+ let docs_href = localized_path("/docs/mcp", i18n.get_locale_untracked());
view! {
-
"Connect an MCP client"
+
- "The hosted endpoint is "
+ {t!(i18n, manual.hosted_endpoint)} " "
{MCP_ENDPOINT}
- ". Read the "
+ ". " {t!(i18n, manual.read)} " "
- "MCP introduction"
+ {t!(i18n, manual.mcp_introduction)}
- " or the "
- "Exodata MCP docs"
+ " " {t!(i18n, manual.or)} " "
+ {t!(i18n, manual.mcp_docs)}
"."
@@ -142,7 +159,7 @@ fn McpSetup() -> impl IntoView {
--url https://exodata.space/mcp"
/>
impl IntoView {
--url https://exodata.space/mcp"
/>
impl IntoView {
label="MCP Inspector"
language="bash"
code="npx @modelcontextprotocol/inspector"
- note="Open the Inspector UI, then connect to the hosted endpoint with Streamable HTTP transport."
+ note=t_string!(i18n, manual.inspector_note)
/>
@@ -193,6 +210,7 @@ fn CommandBox(
code: &'static str,
#[prop(optional)] note: Option<&'static str>,
) -> impl IntoView {
+ let i18n = use_i18n();
let (copied, set_copied) = signal(false);
view! {
@@ -204,7 +222,11 @@ fn CommandBox(
class="homepage-command-box__copy"
on:click=move |_| copy_code_to_clipboard(code, set_copied)
>
- {move || if copied.get() { "Copied" } else { "Copy" }}
+ {move || if copied.get() {
+ t_string!(i18n, manual.copied)
+ } else {
+ t_string!(i18n, manual.copy)
+ }}
{code}
@@ -215,35 +237,37 @@ fn CommandBox(
#[component]
fn AgentInteractionCard() -> impl IntoView {
+ let i18n = use_i18n();
+ let locale = i18n.get_locale_untracked();
view! {
-