diff --git a/app/assets/stylesheets/application.bootstrap.scss b/app/assets/stylesheets/application.bootstrap.scss index 35c89cd..0383514 100644 --- a/app/assets/stylesheets/application.bootstrap.scss +++ b/app/assets/stylesheets/application.bootstrap.scss @@ -20,7 +20,13 @@ $bootstrap-icons-font-dir: '/fonts'; @import 'base'; @import 'components/navbar'; +@import 'components/sections'; @import 'components/cards'; +@import 'components/stage-badge'; +@import 'components/terminal'; +@import 'components/logo-wall'; +@import 'components/browser-frame'; +@import 'pages/home'; @import 'pages/hardware'; @import 'pages/calculator'; @import 'pages/openwall'; diff --git a/app/assets/stylesheets/components/_browser-frame.scss b/app/assets/stylesheets/components/_browser-frame.scss new file mode 100644 index 0000000..621b56e --- /dev/null +++ b/app/assets/stylesheets/components/_browser-frame.scss @@ -0,0 +1,35 @@ +// CSS "browser chrome" wrapper that makes plain screenshots look intentional. +.browser-frame { + border: 1px solid $border-color; + border-radius: $border-radius-lg; + box-shadow: $box-shadow-sm; + overflow: hidden; + + .browser-frame-bar { + align-items: center; + background: $body-tertiary-bg; + border-bottom: 1px solid $border-color; + display: flex; + gap: .3rem; + padding: .45rem .75rem; + + span { + background: $border-color; + border-radius: 50%; + height: .55rem; + width: .55rem; + } + + .browser-frame-title { + color: $body-secondary-color; + font-family: $font-family-monospace; + font-size: .7rem; + margin-inline-start: .5rem; + } + } + + img { + display: block; + width: 100%; + } +} diff --git a/app/assets/stylesheets/components/_cards.scss b/app/assets/stylesheets/components/_cards.scss index 29ea4c0..d985f20 100644 --- a/app/assets/stylesheets/components/_cards.scss +++ b/app/assets/stylesheets/components/_cards.scss @@ -1,3 +1,64 @@ +// Pillar cards (homepage) and project cards (ecosystem/labs). +.pillar-card, +.project-card { + height: 100%; + transition: border-color .15s ease-out, box-shadow .15s ease-out, transform .15s ease-out; + + a { + text-decoration: none; + } + + &:hover, &:focus-within { + border-color: rgba($indigo, .45); + box-shadow: $box-shadow; + + @media (prefers-reduced-motion: no-preference) { + transform: translateY(-2px); + } + } +} + +.pillar-card .icon-chip { + align-items: center; + background: rgba($indigo, .08); + border-radius: $border-radius-lg; + color: $indigo; + display: inline-flex; + font-size: 1.5rem; + height: 3rem; + justify-content: center; + width: 3rem; + + &.icon-chip--accent { + background: rgba($accent, .15); + color: shade-color($accent, 35%); + } +} + +.project-card .card-title { + font-family: $font-family-monospace; + font-size: 1rem; +} + +// Live indicator dot (Open Wall strips). +.live-dot { + animation: live-pulse 2s infinite; + background: $accent; + border-radius: 50%; + display: inline-block; + height: .5rem; + width: .5rem; +} + +@keyframes live-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: .35; } +} + +@media (prefers-reduced-motion: reduce) { + .live-dot { animation: none; } +} + // Team member cards: the social icons float over the top edge of the card. .card-body { position: relative; diff --git a/app/assets/stylesheets/components/_logo-wall.scss b/app/assets/stylesheets/components/_logo-wall.scss new file mode 100644 index 0000000..044a003 --- /dev/null +++ b/app/assets/stylesheets/components/_logo-wall.scss @@ -0,0 +1,26 @@ +// Partner/integrator logo wall: white tiles normalize the mixed PNG set. +.logo-wall { + .logo-tile { + align-items: center; + background: #fff; + border: 1px solid $border-color; + border-radius: $border-radius; + display: flex; + height: 4.5rem; + justify-content: center; + padding: .75rem 1rem; + + img { + filter: grayscale(1) opacity(.7); + max-height: 100%; + max-width: 100%; + object-fit: contain; + transition: filter .15s ease-out; + } + + &:hover img, + &:focus-within img { + filter: none; + } + } +} diff --git a/app/assets/stylesheets/components/_sections.scss b/app/assets/stylesheets/components/_sections.scss new file mode 100644 index 0000000..abb02e8 --- /dev/null +++ b/app/assets/stylesheets/components/_sections.scss @@ -0,0 +1,26 @@ +// Full-bleed homepage/landing sections. Mark dark ones with data-bs-theme="dark" +// in the markup so Bootstrap color tokens flip accordingly. +.section--ink { + background: $ink; + color: rgba(255, 255, 255, .85); + + // Faint engineering dot-grid, used on the hero. + &.section--grid { + background-image: radial-gradient(rgba(255, 255, 255, .07) 1px, transparent 1px); + background-size: 24px 24px; + } + + :focus-visible { + outline-color: #fff; + } +} + +.section--alt { + background: $body-tertiary-bg; +} + +// Standard inner-page header band. +.page-header { + background: $body-tertiary-bg; + border-bottom: 1px solid $border-color; +} diff --git a/app/assets/stylesheets/components/_stage-badge.scss b/app/assets/stylesheets/components/_stage-badge.scss new file mode 100644 index 0000000..e736884 --- /dev/null +++ b/app/assets/stylesheets/components/_stage-badge.scss @@ -0,0 +1,43 @@ +// Text badge for project/SoC development stages (NEQ/R&D/HLP/WIP/MVP/DONE). +// Replaces the raw stage SVGs in tables and cards; SVG pictograms stay in the legend page. +.stage-badge { + align-items: center; + border: 1px solid $border-color; + border-radius: 50rem; + display: inline-flex; + font-family: $font-family-monospace; + font-size: .6875rem; + font-weight: 600; + gap: .35em; + letter-spacing: .05em; + line-height: 1; + padding: .25em .65em; + text-transform: uppercase; + white-space: nowrap; + + &::before { + border-radius: 50%; + content: ''; + height: .5em; + width: .5em; + } + + @each $stage, $color in $stage-colors { + &.stage-badge--#{$stage} { + border-color: rgba($color, .5); + color: shade-color($color, 25%); + + &::before { + background: $color; + } + } + } +} + +[data-bs-theme='dark'] .stage-badge { + @each $stage, $color in $stage-colors { + &.stage-badge--#{$stage} { + color: tint-color($color, 35%); + } + } +} diff --git a/app/assets/stylesheets/components/_terminal.scss b/app/assets/stylesheets/components/_terminal.scss new file mode 100644 index 0000000..74e926d --- /dev/null +++ b/app/assets/stylesheets/components/_terminal.scss @@ -0,0 +1,68 @@ +// Terminal-style code block with a copy button. Markup: shared/_terminal partial. +.terminal { + background: $ink; + border-radius: $border-radius-lg; + color: #e7ebf5; + direction: ltr; // shell commands stay LTR even in RTL locales + font-family: $font-family-monospace; + overflow: hidden; + text-align: left; + + .terminal-header { + align-items: center; + background: $ink-2; + color: rgba(255, 255, 255, .55); + display: flex; + font-size: .75rem; + gap: .5rem; + padding: .5rem .75rem; + + .dots { + display: inline-flex; + gap: .3rem; + + span { + background: rgba(255, 255, 255, .25); + border-radius: 50%; + height: .55rem; + width: .55rem; + } + } + + .btn-copy { + --bs-btn-color: rgba(255, 255, 255, .55); + --bs-btn-hover-color: #fff; + margin-inline-start: auto; + } + } + + pre { + margin: 0; + overflow-x: auto; + padding: .875rem 1rem; + + code { + color: inherit; + font-size: .875rem; + } + } + + .prompt { + color: rgba(255, 255, 255, .45); + user-select: none; + } +} + +// Blinking cursor — allowed in exactly one place (hero microline). +.cursor-blink::after { + animation: cursor-blink 1.1s steps(2) infinite; + content: '▌'; +} + +@keyframes cursor-blink { + 50% { opacity: 0; } +} + +@media (prefers-reduced-motion: reduce) { + .cursor-blink::after { animation: none; } +} diff --git a/app/assets/stylesheets/pages/_home.scss b/app/assets/stylesheets/pages/_home.scss new file mode 100644 index 0000000..d2d05b8 --- /dev/null +++ b/app/assets/stylesheets/pages/_home.scss @@ -0,0 +1,108 @@ +// Homepage sections. +.hero { + h1 { + font-size: clamp(1.9rem, 4.5vw, 3.2rem); + font-weight: 700; + text-wrap: balance; + } + + .hero-lede { + color: rgba(255, 255, 255, .78); + max-width: 60ch; + } + + .hero-microline { + color: rgba(255, 255, 255, .55); + font-family: $font-family-monospace; + font-size: .875rem; + } +} + +// Live Open Wall mosaic (3x2 tiles). +.wall-mosaic { + display: grid; + gap: 8px; + grid-template-columns: repeat(3, 1fr); + + .wall-tile { + background: $ink-2; + border-radius: $border-radius; + display: block; + overflow: hidden; + position: relative; + + img { + aspect-ratio: 16 / 9; + display: block; + height: 100%; + object-fit: cover; + width: 100%; + } + + &.wall-tile--cta { + align-items: center; + aspect-ratio: 16 / 9; + border: 1px dashed rgba(255, 255, 255, .3); + color: rgba(255, 255, 255, .7); + display: flex; + font-size: .8125rem; + justify-content: center; + padding: .5rem; + text-align: center; + text-decoration: none; + + &:hover, &:focus { + border-color: $accent; + color: #fff; + } + } + } + + .wall-caption { + background: linear-gradient(transparent, rgba(15, 20, 34, .85)); + bottom: 0; + color: rgba(255, 255, 255, .85); + font-family: $font-family-monospace; + font-size: .65rem; + inset-inline: 0; + overflow: hidden; + padding: 1rem .5rem .25rem; + position: absolute; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +// "Runs on silicon by" strip. +.silicon-strip { + color: $body-secondary-color; + font-family: $font-family-monospace; + font-size: .875rem; + letter-spacing: .06em; + text-transform: uppercase; +} + +// Stats band figures. +.stat-figure { + .stat-value { + color: #fff; + font-family: $font-family-monospace; + font-size: 2.25rem; + font-weight: 600; + line-height: 1.1; + } + + .stat-label { + color: rgba(255, 255, 255, .6); + font-size: .875rem; + } +} + +// Liberation story panels. +.story-step .story-index { + color: rgba($indigo, .35); + font-family: $font-family-monospace; + font-size: 2rem; + font-weight: 600; + line-height: 1; +} diff --git a/app/assets/stylesheets/pages/_openwall.scss b/app/assets/stylesheets/pages/_openwall.scss index 93fa9fc..b34c5b5 100644 --- a/app/assets/stylesheets/pages/_openwall.scss +++ b/app/assets/stylesheets/pages/_openwall.scss @@ -10,3 +10,11 @@ color: #f1f1f1ff; a { color: #ffcc00; } } + +// Snapshot card metadata in mono, for the data-plate look. +.snapshot-card { + .card-title { + font-family: $font-family-monospace; + font-size: .9375rem; + } +} diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 8997241..52897db 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -9,11 +9,36 @@ def about render 'pages/our_channels' end + def business + @page_title = t('pages.business.title') + render 'pages/business' + end + + def community + @page_title = t('pages.community.title') + render 'pages/community' + end + + def donate + @page_title = t('pages.donate.title') + render 'pages/donate' + end + + def ecosystem + @page_title = t('pages.ecosystem.title') + render 'pages/ecosystem' + end + def firmware_partitions_calculation @page_title = t('pages.firmware_partitions_calculation.title') render 'pages/firmware_partitions_calculation' end + def get_started + @page_title = t('pages.get_started.title') + render 'pages/get_started' + end + def green_life @page_title = t('pages.green_life.title') render 'pages/green_life' @@ -24,6 +49,26 @@ def high_resolution_timer render 'pages/high_resolution_timer' end + # The homepage the relaunch is building towards. Reachable by URL, but the + # root route and the navigation still point at #introduction until the cutover. + # + # The counts are read rather than written into the copy so they cannot go + # stale, and the page renders with all of them at zero -- a fresh checkout has + # an empty database and must not 500. + def home + @page_title = t('pages.home.title') + @meta_description = t('site.default_meta_description') + @wall_snapshots = Snapshot.latest_per_camera(limit: 5) + @soc_count = Soc.count + @vendor_names = Vendor.order(:name).pluck(:name) + render 'pages/home' + end + + def low_latency + @page_title = t('pages.low_latency.title') + render 'pages/low_latency' + end + def introduction @page_title = t('pages.introduction.title') render 'pages/introduction' diff --git a/app/controllers/snapshots_controller.rb b/app/controllers/snapshots_controller.rb index 9ddc34e..588c980 100644 --- a/app/controllers/snapshots_controller.rb +++ b/app/controllers/snapshots_controller.rb @@ -7,11 +7,7 @@ class SnapshotsController < ApplicationController def index page = params[:page] || 1 - sql = 'SELECT s1.* FROM snapshots s1 LEFT JOIN snapshots s2' \ - ' ON (s1.mac_address = s2.mac_address AND s1.created_at < s2.created_at)' \ - ' WHERE s2.id IS NULL AND s1.created_at > SUBDATE(NOW(), INTERVAL 1 DAY)' \ - ' ORDER BY created_at DESC' - @snapshots = Kaminari.paginate_array(Snapshot.find_by_sql(sql)).page(page).per(18) + @snapshots = Kaminari.paginate_array(Snapshot.latest_per_camera).page(page).per(18) @page_title = "Open Wall, page #{page}" render 'snapshots/index' end diff --git a/app/helpers/pages_helper.rb b/app/helpers/pages_helper.rb index 848f7e2..33f7a3c 100644 --- a/app/helpers/pages_helper.rb +++ b/app/helpers/pages_helper.rb @@ -1,7 +1,84 @@ # frozen_string_literal: true +# Helpers for the static marketing pages: the page title, and the partner wall. module PagesHelper + # The partner wall, in Ruby rather than hardcoded into a template. + # + # Both lists were transcribed from the wall on /introduction as it stands + # today, not from an older copy: entries that page has commented out are + # commented out here too, with the same URLs, so nothing appears or disappears + # silently when /introduction is eventually retired. Uncommenting a line here + # is how one comes back. + # + # Shown to every visitor. + INTERNATIONAL_PARTNERS = [ + { name: 'Open Source Collective', url: 'https://www.oscollective.org/', img: 'partners/osc_mini.png' }, + { name: 'GitHub', url: 'https://github.com/', img: 'partners/github_mini.png' }, + { name: 'RunCam', url: 'https://runcam.com/', img: 'partners/runcam_mini.png' }, + { name: 'CCDCAM', url: 'https://ccdcam.com/', img: 'partners/ccdcam_mini.png' }, + { name: 'wfb-ng', url: 'https://github.com/svpcom/wfb-ng/', img: 'partners/wfb-ng_mini.png' }, + { name: 'RubyFPV', url: 'https://rubyfpv.com/', img: 'partners/rubyfpv_mini.png' }, + { name: 'Mario FPV', url: 'https://www.youtube.com/@mariofpv', img: 'partners/mariofpv_mini.png' }, + { name: 'Linux Chenxing', url: 'https://linux-chenxing.org/', img: 'partners/linuxchenxing_mini.png' }, + { name: 'TUDSaT', url: 'https://www.tudsat.space/', img: 'partners/tudsat_mini.png' }, + { name: 'WüSpace', url: 'https://wuespace.de/', img: 'partners/wuespace_mini.png' }, + { name: 'Really', url: 'https://opencollective.com/really-541ee976', img: 'partners/really_mini.png' } + # Commented out on /introduction, so commented out here: + # { name: 'EMAX', url: 'https://emaxmodel.com/', img: 'partners/emax_mini.png' }, + # { name: 'GoodCam', url: 'https://www.goodcam.io/', img: 'partners/goodcam_mini.png' }, + # + # partners/baresip_mini.png exists as an asset but has never been on the + # wall. Left off rather than introduced by a refactor. + ].freeze + + # Integrators are territory-specific: these serve Russia and are shown only to + # Russian-language visitors. Showing them to everyone was explicitly not + # wanted, and the reverse -- gating them in CSS on /introduction with + # `html:not([lang="ru"])` -- never worked, because no logo on that page ever + # carried the `ru` class the rule selects on. + # + # NOTE FOR REVIEW: this whole block is currently commented out on + # /introduction, so no visitor sees any of it today. Rendering it for :ru + # brings it back. That is what the relaunch plan asks for, but it is a content + # decision rather than a technical one -- say so if it should stay hidden. + RU_INTEGRATORS = [ + { name: 'GoodCam', url: 'https://www.goodcam.io/', img: 'partners/goodcam_mini.png' }, + { name: 'SkyCam', url: 'https://skycam.cam/', img: 'partners/skycam_mini.png' }, + { name: 'Vixand', url: 'https://vixand.ru/', img: 'partners/vixand_mini.png' }, + { name: 'Improve IT', url: 'https://3it.ru/', img: 'partners/improve_mini.png' }, + { name: 'UfaNet', url: 'https://www.ufanet.ru/', img: 'partners/ufanet_mini.png' }, + { name: 'Dvor24', url: 'https://dvor24.ru/', img: 'partners/dvor24_mini.png' }, + { name: 'Sputnik', url: 'https://sputnik.systems/', img: 'partners/sputnik_mini.png' }, + { name: 'Techno-Shield', url: 'https://msvoko.ru/', img: 'partners/techno-shield_mini.png' }, + { name: 'KeyTelecom', url: 'https://keytele.com/', img: 'partners/keytelecom_mini.png' }, + { name: 'AnyCam', url: 'https://anycam.io/', img: 'partners/anycam_mini.png' }, + { name: 'WebGlazok', url: 'https://webglazok.com/', img: 'partners/webglazok_mini.png' }, + { name: 'Yucca', url: 'https://yucca.app/en', img: 'partners/yucca_mini.png' }, + { name: 'IPEYE', url: 'https://ipeye.ru/', img: 'partners/ipeye_mini.png' }, + { name: 'VTL', url: 'https://vtl.su/#rec35109538', img: 'partners/vtl_mini.png' }, + { name: 'S-Video', url: 'https://www.cctvsp.ru/cctv/openipc', img: 'partners/s-video_mini.png' }, + { name: 'MyWiFi', url: 'https://xn--80aaaf0bh2e7a5c.xn--p1ai/', img: 'partners/mywifi-cc_mini.png' }, + { name: 'AlarmSystem', + url: 'https://alarmsystem-cctv.ru/product-category/cctv-products/cctv-cameras/ip-cameras-cctv/' \ + '?swoof=1&product_brands=openipc&really_curr_tax=189-product_cat', + img: 'partners/alarmsystem_mini.png' } + # Commented out on /introduction, so commented out here: + # { name: 'MegaCam', url: 'https://megacam.kz/', img: 'partners/megacam_mini.png' }, + # { name: 'Dozor', url: 'https://dozor-smart.ru/', img: 'partners/dozor_mini.png' }, + # { name: 'Flagman', url: 'https://flagman.org/', img: 'partners/flagman_mini.png' }, + # { name: 'Meldana', url: 'https://meldana.com/', img: 'partners/meldana_mini.png' }, + # { name: 'Binary Machines', url: 'https://bmachines.ru/', img: 'partners/binary-machines_mini.png' }, + # { name: 'Expo Electronica', url: 'https://expoelectronica.ru/en/', img: 'partners/expo-electronica_mini.png' }, + # { name: 'GAINS', url: 'https://gains.company/', img: 'partners/gain_mini.png' } + ].freeze + def page_title [@page_title, 'OpenIPC'].join(' - ') end + + def partner_logos + logos = INTERNATIONAL_PARTNERS + logos += RU_INTEGRATORS if I18n.locale.eql?(:ru) + logos + end end diff --git a/app/javascript/application.js b/app/javascript/application.js index 95fb502..7fa3a1e 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -21,6 +21,7 @@ import initExternalLinks from './src/external-links' import initTimestamps from './src/timestamps' import initConfirms from './src/confirms' import initHeifViewer from './src/heif-viewer' +import initCopy from './src/copy' // DOMContentLoaded, not window.onload, which waits for every image and on the // Open Wall meant the page sat unresponsive until the whole gallery had loaded. @@ -32,4 +33,5 @@ document.addEventListener('DOMContentLoaded', () => { initTimestamps() initConfirms() initHeifViewer() + initCopy() }) diff --git a/app/javascript/src/copy.js b/app/javascript/src/copy.js new file mode 100644 index 0000000..7035b4b --- /dev/null +++ b/app/javascript/src/copy.js @@ -0,0 +1,17 @@ +// Copy-to-clipboard for terminal blocks and anything with [data-copy-target]. +export default function initCopy() { + document.addEventListener('click', ev => { + const btn = ev.target.closest('[data-copy-target]') + if (!btn) return + + const source = document.querySelector(btn.dataset.copyTarget) + if (!source) return + + navigator.clipboard.writeText(source.innerText.trim()).then(() => { + const icon = btn.querySelector('i') || btn + const original = icon.className + icon.className = 'bi bi-check-lg' + setTimeout(() => { icon.className = original }, 1500) + }) + }) +} diff --git a/app/models/snapshot.rb b/app/models/snapshot.rb index 8a2dcc0..819b4f4 100644 --- a/app/models/snapshot.rb +++ b/app/models/snapshot.rb @@ -12,6 +12,24 @@ class TooSoon < StandardError INTERVAL_LIMIT = 15.minutes + # The newest snapshot from each camera seen in the last 24 hours, newest + # first. Lived inline in SnapshotsController#index; the homepage mosaic wants + # the same list, and two copies of a correlated subquery is one too many. + # + # The LEFT JOIN ... WHERE s2.id IS NULL is a greatest-n-per-group: a row + # survives only when no newer row exists for its MAC. + # + # limit is interpolated after to_i, not bound, because it lands in a LIMIT + # clause where a bind parameter is not accepted; to_i is what makes that safe. + def self.latest_per_camera(limit: nil) + sql = 'SELECT s1.* FROM snapshots s1 LEFT JOIN snapshots s2' \ + ' ON (s1.mac_address = s2.mac_address AND s1.created_at < s2.created_at)' \ + ' WHERE s2.id IS NULL AND s1.created_at > SUBDATE(NOW(), INTERVAL 1 DAY)' \ + ' ORDER BY created_at DESC' + sql += " LIMIT #{limit.to_i}" if limit + find_by_sql(sql) + end + # Uploads may be HEIF (HEVC/AVC) as well as JPEG. Render every variant as JPEG # so the wall displays in all browsers (HEIF is decodable only by Safari) and # stays small. Decoding HEIF sources requires the server's libvips to be built diff --git a/app/views/pages/_donate.html.erb b/app/views/pages/_donate.html.erb index 6eeec37..a034934 100644 --- a/app/views/pages/_donate.html.erb +++ b/app/views/pages/_donate.html.erb @@ -1,11 +1,14 @@ +<%# The band rendered under every page. Its keys live under + pages.donate_band rather than pages.donate, because /donate is now a + page of its own and lazy lookup would collide with it. %>
-

<%= t('.title') %>

-

<%= t('.please_support') %>.

+

<%= t('pages.donate_band.title') %>

+

<%= t('pages.donate_band.please_support') %>.

diff --git a/app/views/pages/business.html.erb b/app/views/pages/business.html.erb new file mode 100644 index 0000000..ef9f9f9 --- /dev/null +++ b/app/views/pages/business.html.erb @@ -0,0 +1,71 @@ +<% content_for :fullwidth, 'yes' %> + +<%= render 'shared/page_header', title: t('.hero_title'), lede: t('.hero_lede') %> + +
+
+ <%# Offerings %> +
+ <% [%w[offer1 bi-life-preserver], %w[offer2 bi-code-square], %w[offer3 bi-box-seam]].each do |key, icon| %> +
+
+
+ +

<%= t(".#{key}_title") %>

+

<%= t(".#{key}_text") %>

+
+
+
+ <% end %> +
+ + <%# Why %> +

<%= t('.why_title') %>

+
+ <% (1..4).each do |i| %> +
+

<%= t(".why#{i}_title") %>

+

<%= t(".why#{i}_text") %>

+
+ <% end %> +
+ + <%# Who builds on it (territory-specific integrator set) %> +

<%= t('.partners_title') %>

+ <%= render 'shared/logo_wall', logos: partner_logos %> + + <%# Engagement steps %> +

<%= t('.how_title') %>

+
+ <% (1..4).each do |i| %> +
+
+
0<%= i %>
+

<%= t(".how#{i}") %>

+
+
+ <% end %> +
+ + <%# Licensing clarity %> +
+ +
+

<%= t('.licensing_title') %>

+

<%= t('.licensing_html') %>

+
+
+
+
+ +<%# Contact %> +
+
+

<%= t('.contact_title') %>

+

<%= t('.contact_text') %>

+

+ business@openipc.org +

+

<%= t('.contact_alt_html') %>

+
+
diff --git a/app/views/pages/community.html.erb b/app/views/pages/community.html.erb new file mode 100644 index 0000000..28e0a9f --- /dev/null +++ b/app/views/pages/community.html.erb @@ -0,0 +1,56 @@ +<% content_for :fullwidth, 'yes' %> + +<%= render 'shared/page_header', title: t('.hero_title'), lede: t('.hero_lede') %> + +
+
+ <%# Channels %> +

<%= t('.channels_title') %>

+
+ <% [['OpenIPC Users (EN)', 'https://t.me/+7LL2kc32SOo5YWYy', t('.channel_en')], + ['OpenIPC & FPV', 'https://t.me/+BMyMoolVOpkzNWUy', t('.channel_fpv')], + ['OpenIPC Users (RU)', 'https://t.me/+Sl2GPoR9G2iJAOCr', t('.channel_ru')], + ['OpenIPC Firmware', 'https://t.me/s/openipc_dev', t('.channel_dev')]].each do |name, url, desc| %> +
+
+
+

<%= name %>

+

<%= desc %>

+
+
+
+ <% end %> +
+
+ +

<%= t('.bot_warning') %>

+
+ + <%# Help etiquette %> +

<%= t('.help_title') %>

+ + + <%# Ways to contribute %> +

<%= t('.contribute_title') %>

+
+ <% [%w[contrib1 bi-bug], %w[contrib2 bi-git], %w[contrib3 bi-cpu], + %w[contrib4 bi-translate], %w[contrib5 bi-box2-heart], %w[contrib6 bi-megaphone]].each do |key, icon| %> +
+

<%= t(".#{key}_title") %>

+

<%= t(".#{key}_text", default: '').presence || t(".#{key}_text_html") %>

+
+ <% end %> +
+ +

<%= t('.money_band_html') %>

+
+
+ +<%= render 'shared/cta_band', + title: t('.hero_title'), + primary_label: t('.cta_join'), primary_href: 'https://t.me/+7LL2kc32SOo5YWYy', + note_html: t('.money_band_html') %> diff --git a/app/views/pages/donate.html.erb b/app/views/pages/donate.html.erb new file mode 100644 index 0000000..63216f3 --- /dev/null +++ b/app/views/pages/donate.html.erb @@ -0,0 +1,37 @@ +<% content_for :fullwidth, 'yes' %> + +<%= render 'shared/page_header', title: t('.hero_title'), lede: t('.hero_lede') %> + +
+
+
+
+

<%= t('.where_title') %>

+

<%= t('.where_text') %>

+

<%= t('pages.support_open_source.contribute_text3') %>

+
+
+
+
+

<%= t('.oc_title') %>

+

<%= t('.oc_text_html') %>

+ <%= t('.oc_button') %> +
+
+
+
+

<%= t('.crypto_title') %>

+

<%= t('.crypto_text_html') %>

+
+
+
+
+ +
+ +

<%= t('.business_band_html') %>

+
+ +

<%= t('.thanks') %>

+
+
diff --git a/app/views/pages/ecosystem.html.erb b/app/views/pages/ecosystem.html.erb new file mode 100644 index 0000000..646a067 --- /dev/null +++ b/app/views/pages/ecosystem.html.erb @@ -0,0 +1,80 @@ +<% content_for :fullwidth, 'yes' %> + +<%= render 'shared/page_header', title: t('.hero_title'), lede: t('.hero_lede') %> + +<% + gh = 'https://github.com/OpenIPC' + sections = [ + { + title: t('.section_core_title'), text: t('.section_core_text'), + projects: [ + { name: 'firmware', desc: t('.proj_firmware'), repo: "#{gh}/firmware", stage: 'done' }, + { name: 'majestic', desc: t('.proj_majestic'), repo: "#{gh}/majestic", stage: 'done' }, + { name: 'divinus', desc: t('.proj_divinus'), repo: "#{gh}/divinus", stage: 'wip' }, + { name: 'ipctool', desc: t('.proj_ipctool'), repo: "#{gh}/ipctool", stage: 'done' }, + { name: 'coupler', desc: t('.proj_coupler'), repo: "#{gh}/coupler", stage: 'done' }, + { name: 'smolrtsp', desc: t('.proj_smolrtsp'), repo: "#{gh}/smolrtsp", stage: 'done' }, + { name: 'microbe-web', desc: t('.proj_microbe'), repo: "#{gh}/microbe-web", stage: 'done' }, + { name: 'yaml-cli', desc: t('.proj_yamlcli'), repo: "#{gh}/yaml-cli", stage: 'done' }, + { name: 'burn', desc: t('.proj_burn'), repo: "#{gh}/burn", stage: 'done' } + ] + }, + { + title: t('.section_longevity_title'), text: t('.section_longevity_text'), + projects: [ + { name: 'openhisilicon', desc: t('.proj_openhisilicon'), repo: "#{gh}/openhisilicon", stage: 'wip' }, + { name: 'openxiongmai', desc: t('.proj_openxiongmai'), repo: "#{gh}/openxiongmai", stage: 'rnd' }, + { name: 'qemu-hisilicon', desc: t('.proj_qemu'), repo: 'https://github.com/widgetii/qemu-hisilicon', stage: 'wip' } + ] + }, + { + title: t('.section_lowlat_title'), text: t('.section_lowlat_text_html'), + projects: [ + { name: 'waybeam_venc', desc: t('.proj_waybeam'), repo: "#{gh}/waybeam_venc", stage: 'done' }, + { name: 'PixelPilot_rk', desc: t('.proj_pixelpilot'), repo: "#{gh}/PixelPilot_rk", stage: 'done' }, + { name: 'aviateur', desc: t('.proj_aviateur'), repo: "#{gh}/aviateur", stage: 'done' }, + { name: 'telemetry', desc: t('.proj_telemetry'), repo: "#{gh}/telemetry", stage: 'done' }, + { name: 'devourer', desc: t('.proj_devourer'), repo: "#{gh}/devourer", stage: 'rnd' } + ] + }, + { + title: t('.section_tools_title'), text: t('.section_tools_text'), + projects: [ + { name: 'onvif-tt', desc: t('.proj_onvif_tt'), repo: "#{gh}/onvif-tt", stage: 'done' }, + { name: 'rnd-player', desc: t('.proj_rnd_player'), repo: "#{gh}/rnd-player", stage: 'done' } + ] + } + ] +%> + +
+
+

<%= t('.stage_legend_html') %>

+ + <% sections.each do |section| %> +
+

<%= section[:title] %>

+

<%= section[:text] %>

+
+ <% section[:projects].each do |project| %> +
+ <%= render 'shared/project_card', name: project[:name], desc: project[:desc], + repo: project[:repo], stage: project[:stage] %> +
+ <% end %> +
+
+ <% end %> + +
+

<%= t('.section_community_title') %>

+

<%= t('.section_community_text_html') %>

+
+
+
+ +<%= render 'shared/cta_band', + title: t('pages.community.contribute_title'), + primary_label: t('pages.community.title'), primary_href: '/community', + secondary_label: 'GitHub', secondary_href: 'https://github.com/OpenIPC', + note_html: t('.contribute_cta_html') %> diff --git a/app/views/pages/get_started.html.erb b/app/views/pages/get_started.html.erb new file mode 100644 index 0000000..8490a2c --- /dev/null +++ b/app/views/pages/get_started.html.erb @@ -0,0 +1,91 @@ +<% content_for :fullwidth, 'yes' %> + +<%= render 'shared/page_header', title: t('.hero_title'), lede: t('.hero_lede') %> + +
+
+ <%# Three steps %> +
+
+

1<%= t('.step1_title') %>

+

<%= t('.step1_text_html') %>

+ <%= render 'shared/terminal', id: 'ipctool-cmd', title: 'camera shell', + code: "curl -L -o /tmp/ipctool https://github.com/OpenIPC/ipctool/releases/download/latest/ipctool\nchmod +x /tmp/ipctool && /tmp/ipctool" %> +
+
+

2<%= t('.step2_title') %>

+

<%= t('.step2_text') %>

+

<%= t('.cta_find_soc') %>

+
+
+

3<%= t('.step3_title') %>

+

<%= t('.step3_text') %>

+
+
+ + <%# Honesty box %> +
+ +
+

<%= t('.warning_title') %>

+

<%= t('.warning_text_html') %>

+
+
+ + <%# What you get %> +
+
+

<%= t('.what_title') %>

+

<%= t('.what_text') %>

+

<%= t('.editions_title') %>

+

<%= t('.editions_text') %>

+
+
+

<%= t('.webui_title') %>

+ <%# Four of the screenshots from /web-interface, taken through + WebuiGallery rather than named here. The June draft of this page + hardcoded webui/preview.jpg and three others; the gallery was + reshot in 0f42514 and every one of those filenames stopped + existing, which is what a hardcoded list buys you. First() rather + than a chosen set, so a screen leaving the manifest cannot leave a + hole here either. + + Tile and full-resolution file are a pair: the tile is the 1200px + copy, and data-zoom names the 2560px original for the modal, which + is what stops the zoom being an upscale of a thumbnail. %> +
+ <% WebuiGallery.screens.first(4).each do |screen| %> +
+
+
+ <%= screen.caption %> +
+ <%= image_tag screen.tile, class: 'img-zoom', alt: screen.alt, + loading: 'lazy', decoding: 'async', + data: { zoom: image_path(screen.full) } %> +
+
+ <% end %> +
+
+
+ + <%# Open Wall + help %> +
+
+

<%= t('.wall_title') %>

+

<%= t('.wall_text_html') %>

+
+
+

<%= t('.stuck_title') %>

+

<%= t('.stuck_text_html') %>

+
+
+
+
+ +<%= render 'shared/cta_band', + title: t('.hero_title'), + primary_label: t('.cta_find_soc'), primary_href: '/supported-hardware' %> + +<%= render 'pages/zoom' %> diff --git a/app/views/pages/home.html.erb b/app/views/pages/home.html.erb new file mode 100644 index 0000000..d5f8461 --- /dev/null +++ b/app/views/pages/home.html.erb @@ -0,0 +1,144 @@ +<% content_for :fullwidth, 'yes' %> + +<%# ---- Hero: tagline + live Open Wall mosaic ---- %> +
+
+
+
+

<%= t('.hero_title') %>

+

<%= t('.hero_lede') %>

+

+ <%= t('.hero_cta_start') %> + <%= t('.hero_cta_low_latency') %> +

+ +
+
+
+ <% @wall_snapshots.each_with_index do |snapshot, idx| %> + + <%= image_tag snapshot.file.variant(:thumb), alt: t('.wall_snapshot_alt'), + loading: (idx.zero? ? 'eager' : 'lazy') %> + <%= snapshot.soc.upcase %> · <%= snapshot.sensor.upcase %> + + <% end %> + <% (5 - @wall_snapshots.size).times do %> + + <%= image_tag 'no-signal.webp', alt: t('snapshots.index.no_signal'), loading: 'lazy' %> + + <% end %> + <%= t('.wall_cta') %> +
+

+ + <%= t('.wall_live') %> +

+
+
+
+
+ +<%# ---- Silicon strip ---- %> +<% if @vendor_names.any? %> +
+
+
+ <%= t('.silicon_title') %>: + <% @vendor_names.each do |name| %> + <%= name %> + <% end %> +
+
+
+<% end %> + +<%# ---- Liberation story ---- %> +
+
+

<%= t('.story_title') %>

+
+ <% (1..3).each do |i| %> +
+
+
0<%= i %>
+

<%= t(".story_step#{i}_title") %>

+

<%= t(".story_step#{i}_text") %>

+
+
+ <% end %> +
+

+ <%= t('.story_cta') %> +

+
+
+ +<%# ---- Platform pillars ---- %> +
+
+
+

<%= t('.pillars_title') %>

+

<%= t('.pillars_lede') %>

+
+
+
<%= render 'shared/pillar_card', icon: 'bi-cpu', title: t('.pillar_firmware_title'), text: t('.pillar_firmware_text'), href: '/get-started' %>
+
<%= render 'shared/pillar_card', icon: 'bi-broadcast', title: t('.pillar_low_latency_title'), text: t('.pillar_low_latency_text'), href: '/low-latency' %>
+
<%= render 'shared/pillar_card', icon: 'bi-wrench-adjustable', title: t('.pillar_tools_title'), text: t('.pillar_tools_text'), href: '/ecosystem' %>
+
<%= render 'shared/pillar_card', icon: 'bi-shield-check', title: t('.pillar_longevity_title'), text: t('.pillar_longevity_text'), href: '/ecosystem', accent: true %>
+
+
+
+ +<%# ---- Stats band (live numbers from the database; evergreen floors elsewhere) ---- %> +
+
+
+ <% if @soc_count.positive? %> +
+
<%= @soc_count %>
+
<%= t('.stats_socs') %>
+
+
+
<%= @vendor_names.size %>
+
<%= t('.stats_vendors') %>
+
+ <% end %> +
+
140+
+
<%= t('.stats_repos') %>
+
+
+
<%= I18n.available_locales.size %>
+
<%= t('.stats_languages') %>
+
+
+
2019
+
<%= t('.stats_since') %>
+
+
+
+
+ +<%# ---- Partners (integrator set is territory-specific) ---- %> +
+
+

<%= t('.partners_title') %>

+ <%= render 'shared/logo_wall', logos: partner_logos %> +
+
+ +<%# ---- For Business teaser ---- %> +
+
+

<%= t('.business_title') %>

+

<%= t('.business_text') %>

+ <%= t('.business_cta') %> +
+
+ +<%# ---- Closing CTA ---- %> +<%= render 'shared/cta_band', + title: t('.cta_title'), + primary_label: t('.cta_primary'), primary_href: '/get-started', + secondary_label: t('.cta_secondary'), secondary_href: 'https://t.me/openipc', + note_html: t('.cta_donate_html') %> diff --git a/app/views/pages/low_latency.html.erb b/app/views/pages/low_latency.html.erb new file mode 100644 index 0000000..df609ee --- /dev/null +++ b/app/views/pages/low_latency.html.erb @@ -0,0 +1,90 @@ +<% content_for :fullwidth, 'yes' %> + +<%= render 'shared/page_header', title: t('.hero_title'), lede: t('.hero_lede') %> + +
+
+ <%# How the link works %> +

<%= t('.how_title') %>

+
+
+
+ +

<%= t('.how_tx_title') %>

+

<%= t('.how_tx_text_html') %>

+
+
+
+
+ +

<%= t('.how_link_title') %>

+

<%= t('.how_link_text_html') %>

+
+
+
+
+ +

<%= t('.how_rx_title') %>

+

<%= t('.how_rx_text_html') %>

+
+
+
+

TX → wfb-ng → RX

+ + <%# FPV %> +
+
+

<%= t('.fpv_title') %>

+

<%= t('.fpv_text') %>

+

+ <%= t('.cta_guide') %> + <%= t('.cta_chat') %> +

+
+
+

<%= t('.robotics_title') %>

+

<%= t('.robotics_text1') %>

+

<%= t('.robotics_text2') %>

+

<%= t('.business_bridge_html') %>

+
+
+ + <%# Latency, honestly %> +

<%= t('.latency_title') %>

+

<%= t('.latency_intro') %>

+
+ + + + + + + + + + + + +
<%= t('.latency_column_config') %><%= t('.latency_column_g2g') %>
<%= t('.latency_720p60') %>~60 ms
<%= t('.latency_1080p60') %>~80 ms
<%= t('.latency_1080p30') %>~100 ms
+
+

<%= t('.latency_note') %>

+ + <%# Hardware + credits %> +
+
+

<%= t('.hardware_title') %>

+

<%= t('.hardware_text_html') %>

+
+
+

<%= t('.credits_title') %>

+

<%= t('.credits_text') %>

+
+
+
+
+ +<%= render 'shared/cta_band', + title: t('.fpv_title'), + primary_label: t('.cta_guide'), primary_href: 'https://github.com/OpenIPC/wiki/blob/master/en/fpv.md', + secondary_label: t('.cta_chat'), secondary_href: 'https://t.me/+BMyMoolVOpkzNWUy', + note_html: t('.business_bridge_html') %> diff --git a/app/views/shared/_cta_band.html.erb b/app/views/shared/_cta_band.html.erb new file mode 100644 index 0000000..91b7d58 --- /dev/null +++ b/app/views/shared/_cta_band.html.erb @@ -0,0 +1,15 @@ +<%# Dark call-to-action band. Locals: title, primary_label, primary_href, secondary_label/secondary_href (optional), note_html (optional). %> +
+
+

<%= title %>

+

+ <%= primary_label %> + <% if local_assigns[:secondary_label].present? %> + <%= secondary_label %> + <% end %> +

+ <% if local_assigns[:note_html].present? %> +

<%= note_html %>

+ <% end %> +
+
diff --git a/app/views/shared/_logo_wall.html.erb b/app/views/shared/_logo_wall.html.erb new file mode 100644 index 0000000..e4eddba --- /dev/null +++ b/app/views/shared/_logo_wall.html.erb @@ -0,0 +1,10 @@ +<%# Partner/integrator logo wall. Locals: logos (array of {name:, url:, img:}). %> +
+ <% logos.each do |logo| %> +
+ + <%= image_tag logo[:img], alt: "Image: #{logo[:name]} logo", loading: 'lazy' %> + +
+ <% end %> +
diff --git a/app/views/shared/_page_header.html.erb b/app/views/shared/_page_header.html.erb new file mode 100644 index 0000000..d1d6044 --- /dev/null +++ b/app/views/shared/_page_header.html.erb @@ -0,0 +1,9 @@ +<%# Standard inner-page header band. Locals: title, lede (optional). %> + diff --git a/app/views/shared/_pillar_card.html.erb b/app/views/shared/_pillar_card.html.erb new file mode 100644 index 0000000..8d1a880 --- /dev/null +++ b/app/views/shared/_pillar_card.html.erb @@ -0,0 +1,9 @@ +<%# Homepage pillar card. Locals: icon, title, text, href, accent (optional). %> +
+
+ +

<%= title %>

+

<%= text %>

+ +
+
diff --git a/app/views/shared/_project_card.html.erb b/app/views/shared/_project_card.html.erb new file mode 100644 index 0000000..0e52550 --- /dev/null +++ b/app/views/shared/_project_card.html.erb @@ -0,0 +1,15 @@ +<%# Ecosystem/Labs project card. Locals: name, desc, repo (optional), stage (optional). %> +
+
+
+

<%= name %>

+ <% if local_assigns[:stage].present? %> + <%= render 'shared/stage_badge', stage: stage %> + <% end %> +
+

<%= desc %>

+ <% if local_assigns[:repo].present? %> + <%= repo.sub('https://github.com/', '') %> + <% end %> +
+
diff --git a/app/views/shared/_stage_badge.html.erb b/app/views/shared/_stage_badge.html.erb new file mode 100644 index 0000000..f01a01f --- /dev/null +++ b/app/views/shared/_stage_badge.html.erb @@ -0,0 +1,3 @@ +<%# Project status badge. Locals: stage (neq/rnd/hlp/wip/mvp/done). %> +<% labels = { 'neq' => 'NEQ', 'rnd' => 'R&D', 'hlp' => 'HLP', 'wip' => 'WIP', 'mvp' => 'MVP', 'done' => 'DONE' } %> +"><%= labels[stage.to_s] %> diff --git a/app/views/shared/_terminal.html.erb b/app/views/shared/_terminal.html.erb new file mode 100644 index 0000000..100bbf2 --- /dev/null +++ b/app/views/shared/_terminal.html.erb @@ -0,0 +1,11 @@ +<%# Terminal-style code block. Locals: code, title (optional), id (required for copy). %> +
+
+ + <%= local_assigns[:title] || 'shell' %> + +
+
<%= code %>
+
diff --git a/config/locales/pages.en.yml b/config/locales/pages.en.yml index 865c209..f23cff5 100644 --- a/config/locales/pages.en.yml +++ b/config/locales/pages.en.yml @@ -8,9 +8,116 @@ en: title: Admin Dashboard bandwidth_calculator: title: Bandwidth calculator + business: + contact_alt_html: Prefer chat? Message us on Telegram. + contact_text: Tell us what you are building. An engineer answers, not a sales script. + contact_title: Talk to the team + hero_lede: Commercial support, custom development, and OEM licensing from the core team. + hero_title: Build your product on OpenIPC — with the people who build OpenIPC. + how1: You write to us with the product and the problem. + how2: A scoping call with an engineer. + how3: A pilot with clear deliverables. + how4: Ongoing support or development, sized to your needs. + how_title: How an engagement works + licensing_html: The core of the platform is MIT-licensed — commercial use is welcome. The Majestic streamer is distributed in binary form under the Prosperity license; commercial terms are available. The use of OpenIPC and its components for military purposes is not permitted. + licensing_title: Licensing, plainly + offer1_text: Priority fixes, guaranteed response times, and long-term maintenance of the platforms your product depends on. + offer1_title: Commercial support + offer2_text: SoC bring-up, sensor tuning, NPU features, custom video links, white-label firmware — built to your specification by the people who know the stack best. + offer2_title: Custom development + offer3_text: Commercial licensing for Majestic, branded builds, and integration help for manufacturers shipping OpenIPC-based products. + offer3_title: OEM & licensing + partners_title: Companies already building on OpenIPC + title: For Business + why1_text: Vendor SDKs give you a kernel and a demo. OpenIPC gives you a shipping firmware with streaming, web interface, and updates — you start from 90%, not from zero. + why1_title: Faster than a raw SDK + why2_text: Our mainline-kernel work keeps platforms maintained after vendors abandon them. Your product's lifespan is not capped by a vendor's roadmap. + why2_title: Survives silicon EOL + why3_text: Devices work without our servers — or anyone's. Your cost structure stays yours. + why3_title: No per-unit cloud tax + why4_text: The core is open. Whatever happens to any company, including ours, your product keeps building. + why4_title: Open source is your escrow + why_title: Why companies build on OpenIPC + community: + bot_warning: New members answer two quick questions from our Welcome Bot after joining. If you miss the prompt and cannot post, leave and re-join the group, then watch for the bot's message. + channel_dev: Build notifications straight from GitHub. + channel_en: The international OpenIPC group. + channel_fpv: Everything about OpenIPC-based FPV and video links. + channel_ru: The Russian-speaking OpenIPC group. + channels_title: Where we live + contrib1_text_html: Found a bug? File an issue in the right repository — a good report is a contribution. + contrib1_title: Report bugs + contrib2_text: Fork the code, make your improvement, send a pull request. Small fixes are welcome — that is how most of us started. + contrib2_title: Improve the code + contrib3_text: Run work-in-progress builds on your hardware and tell us what breaks. First adopters are what moves a platform from WIP to DONE. + contrib3_title: Test early builds + contrib4_text_html: Improve the wiki, proofread the website, make it sound native in your language. + contrib4_title: Write and translate + contrib5_text_html: Old boards past end-of-life are gold to us — platforms marked NEQ wait for exactly that hardware. What the stages mean. + contrib5_title: Donate retired hardware + contrib6_text: Write a post, record a video, show your build. Real stories from real users are the best advertising we will never buy. + contrib6_title: Spread the word + contribute_title: Ways to contribute + cta_join: Join the main Telegram group + help1_html: Check the wiki first — installation, FPV, and troubleshooting guides live there. + help2: Search the chat history; most first questions have been answered before. + help3: When you ask, include the SoC, the sensor, what you tried, and a boot log if you have one. Good questions get fast answers. + help_title: Getting help that gets answered + hero_lede: OpenIPC is built in the open by people who answer questions every day. Come say hi. + hero_title: Talk to us. + money_band_html: Prefer to help with money? Fund the work. + title: Community donate: + business_band_html: If your company depends on OpenIPC, the sustainable way to support it is a commercial relationship. See what we offer. + crypto_text_html: TON donations work right inside Telegram via @wallet. + crypto_title: Cryptocurrency + hero_lede: OpenIPC is free to use and expensive to make. Donations buy cameras for new platforms, pay maintainers, and keep the lights on. + hero_title: Fund the work. + oc_button: Donate on Open Collective + oc_text_html: Recurring or one-time, with open and transparent accounting. Recurring backers are permanently listed in the Sponsors section on our GitHub page. + oc_title: Open Collective + thanks: Thank you. It means more than you think. + title: Donate + where_text: Core team members spend their own money on cameras and SDK access to expand hardware support. Funds ease that burden, compensate part-time maintainers, and cover trade shows and vendor meetings where hardware partnerships actually happen. + where_title: Where the money goes + donate_band: please_support: Please consider supporting our projects title: Do you like what we do? + ecosystem: + contribute_cta_html: Pick a project and make it better. Start here. + hero_lede: 'OpenIPC is an ecosystem of more than a hundred open repositories: the firmware itself, the video pipeline, the tools around it, and the research that keeps old silicon alive.' + hero_title: More than firmware. + proj_aviateur: 'Cross-platform receiver: watch the link on Windows, Linux, or macOS.' + proj_burn: Unbricks HiSilicon devices over serial. + proj_coupler: The smooth migration path from vendor firmware to OpenIPC and back. No soldering, no special skills. + proj_devourer: An open foundation for SDR-like receivers built on cheap Wi-Fi hardware. + proj_divinus: An open-source streamer for a growing set of platforms. + proj_firmware: Universal, Buildroot-based firmware for IP cameras — replaces abandoned vendor systems on dozens of SoC families. + proj_ipctool: Identifies the SoC, sensor, and flash chip of nearly any camera — and backs up the stock firmware before you change anything. + proj_majestic: 'The streamer at the heart of the firmware: RTSP, ONVIF, WebRTC, HLS, audio, night modes. Distributed as a binary under the Prosperity license; commercial terms available.' + proj_microbe: The web interface of the firmware. + proj_onvif_tt: 'An ONVIF conformance test tool: verify what a camera actually implements. Free.' + proj_openhisilicon: Keeps the HiSilicon vendor SDK alive on mainline kernels — CVE fixes and modern features after the vendor walks away. + proj_openxiongmai: 'The same idea for Xiongmai SoCs: an open SDK replacement.' + proj_pixelpilot: 'The RX side: turns a Rockchip board into a dedicated ground station with display output.' + proj_qemu: A full HiSilicon IP camera emulator on QEMU — develop and test firmware without hardware on your desk. + proj_rnd_player: 'A player built for media engineers: inspect streams the way a developer needs to.' + proj_smolrtsp: An embeddable RTSP 1.0 server library for cameras and other constrained devices. TCP and UDP, any payload format. + proj_telemetry: Telemetry bridging between flight controllers and the ground. + proj_waybeam: 'The TX side of the link: a flexible video encoder for FPV drones and URLLC devices.' + proj_yamlcli: A small console tool for editing YAML configs from scripts. + section_community_text_html: User-made hardware, mounts, and accessories — like the 3D-printable models tagged openipc on Printables. + section_community_title: Community projects + section_core_text: The operating system, the streamer, and the tools that get you from stock firmware to OpenIPC. + section_core_title: Core firmware + section_longevity_text: 'Vendor SDKs assume vendor kernels: ancient, unpatched, abandoned. We port them to mainline — CVE fixes and modern kernel features long after end-of-life.' + section_longevity_title: Longevity & security + section_lowlat_text_html: The open video link for drones, robots, and teleoperation. Read the overview. + section_lowlat_title: Low latency & URLLC + section_tools_text: Free tools for camera and media engineers — useful even if you never flash our firmware. + section_tools_title: Pro tools + stage_legend_html: Every project carries an honest status badge — the same vocabulary we use for firmware platforms. How stages work. + title: Ecosystem firmware_partitions_calculation: end_address: End address flash_size_mb: Flash size, MB @@ -22,6 +129,28 @@ en: size_hex: Hex size, bytes start_address: Start address title: Firmware Partitions Calculation + get_started: + cta_find_soc: Find your SoC + editions_text: Lite fits 8 MB flash chips and covers the essentials. Ultimate adds extras like cloud streaming, tunnels, and more codecs — it needs 16 MB flash. + editions_title: Two editions + hero_lede: From stock firmware to OpenIPC in about half an hour — with a guide generated for your exact chip. + hero_title: Give your camera a second life. + step1_text_html: Run ipctool on the camera (telnet or serial console) to learn the SoC, the sensor, and the flash chip. A label or FCC ID search works as a fallback. + step1_title: Identify your hardware + step2_text: Look your chip up in the supported hardware table. The wizard assembles a firmware image and a step-by-step installation guide for your exact configuration, right in your browser. + step2_title: Find your SoC + step3_text: Follow the generated guide. It starts with a full backup of the stock firmware — do not skip it. + step3_title: Back up, then flash + stuck_text_html: The community answers questions every day. Ask in the chat — include your SoC, sensor, and a boot log if you have one. + stuck_title: Stuck? + title: Get Started + wall_text_html: One setting in the web interface puts your camera's snapshots on our live Open Wall — say hello to the community. + wall_title: Then join the Open Wall + warning_text_html: Flashing can brick a camera. The guide shows you how to make a full backup first, and Coupler can handle the safe path on supported models. If in doubt, ask in the chat before you flash. + warning_title: Honest warning + webui_title: The web interface + what_text: 'A clean Linux system with the Majestic streamer: RTSP, ONVIF, WebRTC, and HLS out of the box, a web interface, and full root access. Your camera, actually yours.' + what_title: What you get green_life: paragraph1: OpenIPC contributes to sustainability by primarily providing open firmware for IP cameras, directly engaging in the efficient use and repurposing of existing hardware. paragraph2: By extending the lifespan of cameras through customizable firmware updates and features, OpenIPC reduces electronic waste. @@ -31,6 +160,48 @@ en: high_resolution_timer: reload_to_reset: Time shown in milliseconds. Reload page to reset the counter. title: High-Resolution Timer + home: + business_cta: Talk to the team + business_text: Commercial support, custom development, and OEM licensing — from the people who build the platform. + business_title: Building a product on OpenIPC? + cta_donate_html: Prefer to support the work directly? Donate. + cta_primary: Get started + cta_secondary: Join our Telegram + cta_title: Flash your first camera this weekend. + hero_cta_low_latency: Low-latency video + hero_cta_start: Get started + hero_lede: Open firmware and tools for IP cameras, FPV drones, and the devices in between. Your hardware should answer to you — not to a vendor's cloud. + hero_microline: 140+ repositories · dozens of SoC families · no cloud required + hero_title: The open platform for embedded video. + partners_title: Trusted by manufacturers, integrators, and research teams + pillar_firmware_text: An open, Buildroot-based OS for IP cameras across dozens of SoC families, with the Majestic streamer at its core. + pillar_firmware_title: Camera firmware + pillar_longevity_text: 'We keep vendor SDKs alive on mainline kernels: CVE fixes and modern features long after the vendor walks away.' + pillar_longevity_title: Longevity & security + pillar_low_latency_text: An open video link for FPV drones, robots, and teleoperation. Glass-to-glass from about 60 ms on supported hardware. + pillar_low_latency_title: Low-latency video links + pillar_tools_text: ONVIF conformance testing, camera emulation, hardware inspection — free tools we built for ourselves and share with everyone. + pillar_tools_title: Pro tools + pillars_lede: OpenIPC is more than firmware — it is an ecosystem built by people who run it on their own hardware. + pillars_title: One platform, four fronts + silicon_title: Runs on silicon by + stats_languages: languages + stats_repos: open repositories + stats_since: building openly since + stats_socs: supported SoCs + stats_vendors: chip vendors + story_cta: Revive your camera + story_step1_text: Firmware updates stop. Security holes stay. The app disappears from the store. + story_step1_title: The vendor moves on + story_step2_text: Servers get switched off, and a working camera turns into plastic and silicon with nothing to do. + story_step2_title: The cloud goes dark + story_step3_text: The camera boots an open, maintained system — and answers to you. No backdoors. No subscriptions. Less e-waste. + story_step3_title: You flash OpenIPC + story_title: Cameras shouldn't die when their cloud does. + title: The open platform for embedded video + wall_cta: "+ your camera here" + wall_live: 'Live now: cameras running OpenIPC around the world' + wall_snapshot_alt: 'Image: live snapshot from a community camera' introduction: alliance: Alliance button_binaries: Precompiled binary files @@ -57,6 +228,37 @@ en: development: Development gaming: Gaming solutions title: Introduction + low_latency: + business_bridge_html: Building a product on this link? Talk to us — we do custom development and OEM work. + credits_text: The radio link exists thanks to wfb-ng, and the ecosystem grows with friends like RubyFPV and Mario FPV. Open source is a team sport. + credits_title: Standing on open shoulders + cta_chat: Join the FPV chat + cta_guide: Read the build guide + fpv_text: 'A fully open digital FPV system: no locked ecosystem, no paired hardware, no firmware ceiling. Build the air unit from a supported camera, pick your ground station, and tune every stage of the pipeline.' + fpv_title: 'FPV: from lens to goggles' + hardware_text_html: Air units are built from supported cameras and AIO boards — start from the supported hardware list. RunCam and EMAX ship OpenIPC-based FPV hardware out of the box. + hardware_title: Hardware + hero_lede: 'FPV drones, robots, teleoperation: an open ultra-low-latency video stack from camera to screen. Glass-to-glass from about 60 ms on supported hardware.' + hero_title: Open video links for machines that can't wait. + how_link_text_html: wfb-ng broadcasts video, telemetry, and control over commodity Wi-Fi hardware — no association, no retransmit stalls. + how_link_title: The radio link + how_rx_text_html: PixelPilot turns a Rockchip board into a dedicated ground station, and Aviateur receives on Windows, Linux, and macOS. + how_rx_title: RX — your side + how_title: How the link works + how_tx_text_html: An OpenIPC camera with the Waybeam encoder produces the stream and feeds the radio link. + how_tx_title: TX — the air side + latency_1080p30: 1080p30 + latency_1080p60: 1080p60 + latency_720p60: 720p60 + latency_column_config: Configuration + latency_column_g2g: Typical glass-to-glass + latency_intro: 'Numbers depend on SoC, sensor, resolution, exposure, and heat. These are typical figures from our own measurements, not best-case marketing:' + latency_note: Measured with our own open hardware latency meter — built for exactly this job. + latency_title: Latency, honestly + robotics_text1: The same link carries video for industrial robots, AGVs, inspection rigs, and remote operation — anywhere a closed video system would lock you in or a cloud hop would add seconds. + robotics_text2: On supported SoCs, frames carry wall-clock timestamps tied to the sensor frame start — millisecond-class synchronization for multi-camera rigs without extra hardware. + robotics_title: 'Beyond drones: robots and teleoperation' + title: Low-Latency Video majestic_endpoints: moved_html: 'The list of endpoints your camera serves now lives in the camera''s own web interface, under Majestic → Endpoints. Open http://<camera-address>/ in a browser and pick it from the menu.' reason_address: 'your camera''s real address is filled in, so a URL can be copied straight into a player;' diff --git a/config/locales/pages.ru.yml b/config/locales/pages.ru.yml index e797917..372adc0 100644 --- a/config/locales/pages.ru.yml +++ b/config/locales/pages.ru.yml @@ -8,9 +8,116 @@ ru: title: Панель администратора bandwidth_calculator: title: Калькулятор пропускной способности + business: + contact_alt_html: Удобнее в мессенджере? Напишите в Telegram. + contact_text: Расскажите, что вы строите. Ответит инженер, а не скрипт продаж. + contact_title: Свяжитесь с командой + hero_lede: Коммерческая поддержка, заказная разработка и OEM-лицензирование от основной команды. + hero_title: Стройте продукт на OpenIPC — вместе с теми, кто делает OpenIPC. + how1: Вы пишете нам о продукте и задаче. + how2: Скоуп-звонок с инженером. + how3: Пилот с понятными результатами. + how4: Дальше — поддержка или разработка в нужном вам объёме. + how_title: Как устроено сотрудничество + licensing_html: Ядро платформы под лицензией MIT — коммерческое использование приветствуется. Стример Majestic распространяется в бинарном виде под лицензией Prosperity; коммерческие условия доступны. Использование OpenIPC и его компонентов в военных целях запрещено. + licensing_title: Про лицензии, прямо + offer1_text: Приоритетные исправления, гарантированное время реакции и долгосрочное сопровождение платформ, от которых зависит ваш продукт. + offer1_title: Коммерческая поддержка + offer2_text: Запуск новых SoC, настройка сенсоров, NPU-функции, кастомные видеолинки, white-label-прошивки — по вашему ТЗ, руками тех, кто знает стек лучше всех. + offer2_title: Заказная разработка + offer3_text: Коммерческие лицензии Majestic, брендированные сборки и помощь с интеграцией для производителей, выпускающих продукты на OpenIPC. + offer3_title: OEM и лицензирование + partners_title: Компании, которые уже строят на OpenIPC + title: Для бизнеса + why1_text: Вендорский SDK даёт ядро и демку. OpenIPC даёт готовую прошивку со стримингом, веб-интерфейсом и обновлениями — вы стартуете с 90%, а не с нуля. + why1_title: Быстрее, чем голый SDK + why2_text: Наша работа с mainline-ядрами сохраняет платформы живыми после ухода вендора. Срок жизни вашего продукта не ограничен чужим роадмапом. + why2_title: Переживает EOL кремния + why3_text: Устройства работают без наших серверов — и вообще без чьих-либо. Ваша структура затрат остаётся вашей. + why3_title: Без облачного налога с каждого устройства + why4_text: Ядро открыто. Что бы ни случилось с любой компанией, включая нашу, ваш продукт продолжит собираться. + why4_title: Open source — ваша страховка + why_title: Почему компании строят на OpenIPC + community: + bot_warning: Новые участники отвечают на два коротких вопроса нашего приветственного бота. Если пропустили сообщение и не можете писать — выйдите из группы, зайдите снова и дождитесь сообщения бота. + channel_dev: Уведомления о сборках прямо из GitHub. + channel_en: Международная группа OpenIPC. + channel_fpv: Всё про FPV и видеолинки на OpenIPC. + channel_ru: Русскоязычная группа OpenIPC. + channels_title: Где мы живём + contrib1_text_html: Нашли баг? Заведите issue в нужном репозитории — хороший отчёт это уже вклад. + contrib1_title: Сообщайте об ошибках + contrib2_text: Форкните код, внесите улучшение, пришлите pull request. Маленькие исправления приветствуются — большинство из нас так и начинало. + contrib2_title: Улучшайте код + contrib3_text: Запускайте WIP-сборки на своём железе и рассказывайте, что сломалось. Именно первые пользователи двигают платформу от WIP к DONE. + contrib3_title: Тестируйте ранние сборки + contrib4_text_html: Улучшайте вики, вычитывайте сайт, делайте тексты родными для своего языка. + contrib4_title: Пишите и переводите + contrib5_text_html: Старые платы после EOL для нас золото — именно их ждут платформы со статусом NEQ. Что значат стадии. + contrib5_title: Отдавайте старое железо + contrib6_text: Напишите пост, снимите видео, покажите свою сборку. Настоящие истории настоящих пользователей — лучшая реклама, которую мы никогда не купим. + contrib6_title: Рассказывайте о проекте + contribute_title: Как помочь проекту + cta_join: Вступить в основную группу в Telegram + help1_html: Сначала загляните в вики — там живут гайды по установке, FPV и решению проблем. + help2: Поищите в истории чата — на большинство первых вопросов уже отвечали. + help3: В вопросе укажите SoC, сенсор, что уже пробовали и лог загрузки, если есть. Хорошие вопросы получают быстрые ответы. + help_title: Как получить ответ быстро + hero_lede: OpenIPC делается открыто людьми, которые отвечают на вопросы каждый день. Заходите. + hero_title: Поговорите с нами. + money_band_html: Хотите помочь деньгами? Поддержите работу. + title: Сообщество donate: + business_band_html: Если ваша компания зависит от OpenIPC, самый устойчивый способ поддержать проект — коммерческое сотрудничество. Что мы предлагаем. + crypto_text_html: Пожертвования в TON работают прямо в Telegram через @wallet. + crypto_title: Криптовалюта + hero_lede: OpenIPC бесплатен в использовании и дорог в производстве. Пожертвования покупают камеры для новых платформ, оплачивают работу мейнтейнеров и держат свет включённым. + hero_title: Поддержите работу. + oc_button: Пожертвовать на Open Collective + oc_text_html: Разово или регулярно, с открытой и прозрачной бухгалтерией. Постоянные спонсоры навсегда попадают в раздел Sponsors на нашей странице GitHub. + oc_title: Open Collective + thanks: Спасибо. Это значит больше, чем кажется. + title: Поддержать + where_text: Члены основной команды тратят собственные деньги на камеры и доступ к SDK, чтобы расширять поддержку железа. Средства снимают эту нагрузку, оплачивают работу мейнтейнеров и поездки на выставки, где и случаются договорённости с производителями. + where_title: Куда идут деньги + donate_band: please_support: Узнайте, как вы можете его поддержать title: Нравится проект? + ecosystem: + contribute_cta_html: Выберите проект и сделайте его лучше. Начните здесь. + hero_lede: 'OpenIPC — это экосистема из более чем сотни открытых репозиториев: сама прошивка, видеотракт, инструменты вокруг и исследования, которые не дают умереть старому кремнию.' + hero_title: Больше, чем прошивка. + proj_aviateur: 'Кроссплатформенный приёмник: смотрите линк на Windows, Linux и macOS.' + proj_burn: Восстанавливает «окирпиченные» устройства HiSilicon через последовательный порт. + proj_coupler: Плавный переход с вендорской прошивки на OpenIPC и обратно. Без пайки и специальных навыков. + proj_devourer: Открытая основа для SDR-подобных приёмников на дешёвом Wi-Fi-железе. + proj_divinus: Стример с открытым кодом для растущего набора платформ. + proj_firmware: Универсальная прошивка на базе Buildroot для IP-камер — заменяет заброшенные вендорские системы на десятках семейств SoC. + proj_ipctool: Определяет SoC, сенсор и флеш-чип почти любой камеры — и снимает бэкап заводской прошивки до того, как вы что-то измените. + proj_majestic: 'Стример в сердце прошивки: RTSP, ONVIF, WebRTC, HLS, звук, ночные режимы. Распространяется в бинарном виде под лицензией Prosperity; коммерческие условия доступны.' + proj_microbe: Веб-интерфейс прошивки. + proj_onvif_tt: 'Инструмент проверки соответствия ONVIF: узнайте, что камера реализует на самом деле. Бесплатно.' + proj_openhisilicon: Поддерживает вендорский SDK HiSilicon на mainline-ядрах — исправления CVE и новые возможности после ухода производителя. + proj_openxiongmai: 'Та же идея для SoC Xiongmai: открытая замена SDK.' + proj_pixelpilot: 'Сторона RX: превращает плату на Rockchip в выделенную наземную станцию с выводом на экран.' + proj_qemu: Полный эмулятор IP-камер HiSilicon на QEMU — разрабатывайте и тестируйте прошивку без железа на столе. + proj_rnd_player: 'Плеер для медиа-инженеров: разбирайте потоки так, как нужно разработчику.' + proj_smolrtsp: Встраиваемая библиотека RTSP 1.0 для камер и других ограниченных устройств. TCP и UDP, любой формат полезной нагрузки. + proj_telemetry: Мост телеметрии между полётными контроллерами и землёй. + proj_waybeam: 'Сторона TX: гибкий видеоэнкодер для FPV-дронов и URLLC-устройств.' + proj_yamlcli: Маленький консольный инструмент для правки YAML-конфигов из скриптов. + section_community_text_html: Самодельное железо, крепления и аксессуары — например, модели для 3D-печати с тегом openipc на Printables. + section_community_title: Проекты сообщества + section_core_text: Операционная система, стример и инструменты, которые доведут вас от заводской прошивки до OpenIPC. + section_core_title: Ядро платформы + section_longevity_text: 'Вендорские SDK рассчитаны на вендорские ядра: древние, незакрытые, заброшенные. Мы переносим их на mainline — исправления CVE и современные возможности ядра спустя годы после EOL.' + section_longevity_title: Долголетие и безопасность + section_lowlat_text_html: Открытый видеолинк для дронов, роботов и телеуправления. Обзор. + section_lowlat_title: Низкая задержка и URLLC + section_tools_text: Бесплатные инструменты для камерных и медиа-инженеров — полезны, даже если вы никогда не прошьёте нашу прошивку. + section_tools_title: Инструменты для инженеров + stage_legend_html: У каждого проекта — честный значок статуса, тот же словарь, что и у прошивочных платформ. Как работают стадии. + title: Экосистема firmware_partitions_calculation: end_address: Конечный адрес flash_size_mb: Размер флешки, МБ @@ -22,6 +129,28 @@ ru: size_hex: Шестнадцатеричный размер, байт start_address: Начальный адрес title: Расчет разделов прошивки + get_started: + cta_find_soc: Найти свой SoC + editions_text: Lite помещается в 8 МБ флеш-памяти и покрывает базовые задачи. Ultimate добавляет облачные трансляции, туннели и дополнительные кодеки — ей нужно 16 МБ. + editions_title: Две редакции + hero_lede: От заводской прошивки до OpenIPC примерно за полчаса — с инструкцией, собранной под ваш конкретный чип. + hero_title: Подарите камере вторую жизнь. + step1_text_html: 'Запустите ipctool на камере (telnet или консоль UART): он покажет SoC, сенсор и флеш-чип. Этикетка или поиск по FCC ID — запасной вариант.' + step1_title: Определите железо + step2_text: Найдите чип в таблице поддерживаемого оборудования. Мастер соберёт образ прошивки и пошаговую инструкцию установки под вашу конфигурацию прямо в браузере. + step2_title: Найдите свой SoC + step3_text: Следуйте сгенерированной инструкции. Она начинается с полной резервной копии заводской прошивки — не пропускайте этот шаг. + step3_title: Бэкап, потом прошивка + stuck_text_html: Сообщество отвечает на вопросы каждый день. Спросите в чате — укажите SoC, сенсор и, если есть, лог загрузки. + stuck_title: Застряли? + title: Начало работы + wall_text_html: Одна настройка в веб-интерфейсе — и снимки вашей камеры появятся на нашей живой Открытой стене. Передайте привет сообществу. + wall_title: А потом — на Открытую стену + warning_text_html: Прошивка может «окирпичить» камеру. Инструкция показывает, как сначала снять полный бэкап, а Coupler на поддерживаемых моделях делает всё безопасно. Сомневаетесь — спросите в чате до прошивки. + warning_title: Честное предупреждение + webui_title: Веб-интерфейс + what_text: 'Чистый Linux со стримером Majestic: RTSP, ONVIF, WebRTC и HLS из коробки, веб-интерфейс и полный root-доступ. Ваша камера — действительно ваша.' + what_title: Что вы получите green_life: paragraph1: OpenIPC вносит свой вклад в устойчивое развитие, в первую очередь предоставляя открытые микропрограммы для IP-камер, непосредственно участвуя в эффективном использовании и перепрофилировании существующего оборудования. paragraph2: Продлевая срок службы камер за счет настраиваемых обновлений микропрограммного обеспечения и функций, OpenIPC сокращает количество электронных отходов. @@ -31,6 +160,48 @@ ru: high_resolution_timer: reload_to_reset: Время указано в миллисекундах. Перезагрузите страницу, чтобы сбросить счетчик. title: Таймер высокого разрешения + home: + business_cta: Связаться с командой + business_text: Коммерческая поддержка, заказная разработка и OEM-лицензирование — от людей, которые делают платформу. + business_title: Строите продукт на OpenIPC? + cta_donate_html: Хотите поддержать проект напрямую? Пожертвовать. + cta_primary: Начать + cta_secondary: Наш Telegram + cta_title: Прошейте свою первую камеру в эти выходные. + hero_cta_low_latency: Видео с низкой задержкой + hero_cta_start: Начать + hero_lede: Открытая прошивка и инструменты для IP-камер, FPV-дронов и всего, что между ними. Ваше железо должно подчиняться вам, а не облаку производителя. + hero_microline: 140+ репозиториев · десятки семейств SoC · облако не требуется + hero_title: Открытая платформа для встраиваемого видео. + partners_title: Нам доверяют производители, интеграторы и исследователи + pillar_firmware_text: Открытая ОС на базе Buildroot для IP-камер на десятках семейств SoC, со стримером Majestic в основе. + pillar_firmware_title: Прошивка для камер + pillar_longevity_text: 'Мы поддерживаем вендорские SDK на mainline-ядрах: исправления CVE и новые возможности спустя годы после ухода производителя.' + pillar_longevity_title: Долголетие и безопасность + pillar_low_latency_text: Открытый видеолинк для FPV-дронов, роботов и телеуправления. От ~60 мс «от стекла до стекла» на поддерживаемом железе. + pillar_low_latency_title: Видеолинки с низкой задержкой + pillar_tools_text: Проверка ONVIF, эмуляция камер, инспекция железа — бесплатные инструменты, которые мы сделали для себя и отдали всем. + pillar_tools_title: Инструменты для инженеров + pillars_lede: OpenIPC — больше, чем прошивка. Это экосистема, которую делают люди, использующие её на собственном железе. + pillars_title: Одна платформа, четыре направления + silicon_title: Работает на чипах + stats_languages: языков + stats_repos: открытых репозиториев + stats_since: открытая разработка с + stats_socs: поддерживаемых SoC + stats_vendors: производителей чипов + story_cta: Оживить камеру + story_step1_text: Обновления прекращаются. Дыры в безопасности остаются. Приложение исчезает из магазина. + story_step1_title: Производитель уходит + story_step2_text: Серверы выключают, и исправная камера превращается в кусок пластика и кремния без дела. + story_step2_title: Облако гаснет + story_step3_text: Камера загружает открытую, поддерживаемую систему — и подчиняется вам. Без бэкдоров. Без подписок. Меньше электронного мусора. + story_step3_title: Вы прошиваете OpenIPC + story_title: Камера не должна умирать вместе со своим облаком. + title: Открытая платформа для встраиваемого видео + wall_cta: "+ ваша камера здесь" + wall_live: 'Прямо сейчас: камеры на OpenIPC по всему миру' + wall_snapshot_alt: 'Изображение: живой снимок с камеры сообщества' introduction: alliance: Партнеры button_binaries: Готовые бинарные файлы @@ -57,6 +228,37 @@ ru: integrators: Integrators development: Development title: Вступление + low_latency: + business_bridge_html: Строите продукт на этом линке? Напишите нам — мы делаем заказную разработку и OEM. + credits_text: Радиолинк существует благодаря wfb-ng, а экосистема растёт вместе с друзьями — RubyFPV и Mario FPV. Open source — командный спорт. + credits_title: Стоим на открытых плечах + cta_chat: FPV-чат + cta_guide: Гайд по сборке + fpv_text: 'Полностью открытая цифровая FPV-система: никакой закрытой экосистемы, никакого спаренного железа, никакого потолка прошивки. Соберите борт из поддерживаемой камеры, выберите наземную станцию и настройте каждый этап конвейера.' + fpv_title: 'FPV: от объектива до очков' + hardware_text_html: Борт собирается из поддерживаемых камер и AIO-плат — начните со списка поддерживаемого оборудования. RunCam и EMAX выпускают FPV-железо на OpenIPC из коробки. + hardware_title: Железо + hero_lede: 'FPV-дроны, роботы, телеуправление: открытый видеотракт со сверхнизкой задержкой от камеры до экрана. От ~60 мс «от стекла до стекла» на поддерживаемом железе.' + hero_title: Открытые видеолинки для машин, которые не умеют ждать. + how_link_text_html: wfb-ng вещает видео, телеметрию и управление поверх обычного Wi-Fi-железа — без ассоциации и без пауз на повторную передачу. + how_link_title: Радиолинк + how_rx_text_html: PixelPilot превращает плату на Rockchip в выделенную наземную станцию, а Aviateur принимает на Windows, Linux и macOS. + how_rx_title: RX — ваша сторона + how_title: Как устроен линк + how_tx_text_html: Камера с OpenIPC и энкодером Waybeam формирует поток и отдаёт его в радиолинк. + how_tx_title: TX — борт + latency_1080p30: 1080p30 + latency_1080p60: 1080p60 + latency_720p60: 720p60 + latency_column_config: Конфигурация + latency_column_g2g: Типичная задержка «стекло-стекло» + latency_intro: 'Цифры зависят от SoC, сенсора, разрешения, экспозиции и нагрева. Это типичные значения из наших собственных измерений, а не лучший случай из маркетинга:' + latency_note: Измерено нашим собственным открытым аппаратным измерителем задержки, сделанным ровно для этой задачи. + latency_title: Честно про задержку + robotics_text1: Тот же линк несёт видео для промышленных роботов, AGV, инспекционных систем и удалённого управления — везде, где закрытая видеосистема привязала бы вас к вендору, а путь через облако добавил бы секунды. + robotics_text2: На поддерживаемых SoC кадры несут метки времени, привязанные к началу кадра сенсора, — миллисекундная синхронизация многокамерных систем без дополнительного железа. + robotics_title: 'Не только дроны: роботы и телеуправление' + title: Видео с низкой задержкой majestic_endpoints: moved_html: 'Список эндпоинтов, которые отдаёт камера, теперь живёт в веб-интерфейсе самой камеры, в меню Majestic → Endpoints. Откройте в браузере http://<адрес-камеры>/ и выберите этот пункт.' reason_address: 'подставлен настоящий адрес вашей камеры, поэтому ссылку можно скопировать прямо в плеер;' diff --git a/config/locales/pages.zh.yml b/config/locales/pages.zh.yml index 3aff90a..350b767 100644 --- a/config/locales/pages.zh.yml +++ b/config/locales/pages.zh.yml @@ -8,9 +8,116 @@ zh: title: 管理仪表板 bandwidth_calculator: title: 带宽计算器 + business: + contact_alt_html: 更喜欢即时沟通?在 Telegram 上给我们发消息。 + contact_text: 告诉我们你正在做什么产品。回复你的是工程师,不是销售话术。 + contact_title: 与团队沟通 + hero_lede: 由核心团队提供商业支持、定制开发与 OEM 授权。 + hero_title: 在 OpenIPC 上打造你的产品——与开发 OpenIPC 的人一起。 + how1: 你把产品和遇到的问题写信告诉我们。 + how2: 与工程师进行一次需求梳理沟通。 + how3: 一个交付物明确的试点项目。 + how4: 按你的需要提供持续的支持或开发。 + how_title: 合作是怎样进行的 + licensing_html: 平台核心采用 MIT 许可证,欢迎商业使用。Majestic 推流器以二进制形式在 Prosperity 许可证下分发,可另行商谈商业条款。不允许将 OpenIPC 及其组件用于军事用途。 + licensing_title: 授权,直说 + offer1_text: 优先修复、有保障的响应时间,以及对你产品所依赖平台的长期维护。 + offer1_title: 商业支持 + offer2_text: SoC 移植、传感器调优、NPU 功能、定制视频链路、白牌固件——由最熟悉这套技术栈的人按你的规格来做。 + offer2_title: 定制开发 + offer3_text: Majestic 的商业授权、定制品牌固件,以及为出货 OpenIPC 产品的制造商提供集成支持。 + offer3_title: OEM 与授权 + partners_title: 已经基于 OpenIPC 开发的公司 + title: 面向企业 + why1_text: 原厂 SDK 给你的是一个内核和一个演示程序。OpenIPC 给你的是一套可出货的固件,包含推流、Web 界面和更新——你从 90% 起步,而不是从零开始。 + why1_title: 比原始 SDK 更快 + why2_text: 我们在主线内核上的工作让原厂放弃后的平台继续得到维护。你的产品寿命不再受原厂路线图的限制。 + why2_title: 芯片停产后依然可用 + why3_text: 设备无需依赖我们的服务器,也不依赖任何人的服务器。你的成本结构仍然由你掌握。 + why3_title: 没有按台收取的云费用 + why4_text: 核心是开放的。无论任何公司(包括我们)发生什么,你的产品都能继续构建。 + why4_title: 开源就是你的托管保障 + why_title: 企业为什么选择 OpenIPC + community: + bot_warning: 新成员加入后需要回答欢迎机器人提出的两个简单问题。如果你错过了提示且无法发言,请退出并重新加入群组,然后留意机器人的消息。 + channel_dev: 直接来自 GitHub 的构建通知。 + channel_en: OpenIPC 国际群组。 + channel_fpv: 关于基于 OpenIPC 的 FPV 与视频链路的一切。 + channel_ru: OpenIPC 俄语群组。 + channels_title: 我们在哪里活动 + contrib1_text_html: 发现了缺陷?请到对应的仓库提交 issue——一份好的报告本身就是贡献。 + contrib1_title: 报告缺陷 + contrib2_text: Fork 代码,做出改进,提交 pull request。小修小补同样受欢迎——我们大多数人都是这样开始的。 + contrib2_title: 改进代码 + contrib3_text: 在你的硬件上运行开发中的版本,并告诉我们哪里出了问题。第一批尝鲜者正是推动平台从 WIP 走向 DONE 的力量。 + contrib3_title: 测试早期版本 + contrib4_text_html: 完善维基,校对网站文字,让它读起来像母语。 + contrib4_title: 撰写与翻译 + contrib5_text_html: 停产的老板子对我们非常宝贵——标记为 NEQ 的平台等的正是这类硬件。了解各阶段的含义。 + contrib5_title: 捐赠退役硬件 + contrib6_text: 写一篇文章、录一段视频、展示你的作品。真实用户的真实故事,是我们永远买不到的最好宣传。 + contrib6_title: 帮我们传播 + contribute_title: 参与贡献的方式 + cta_join: 加入主 Telegram 群组 + help1_html: 先查阅维基——安装、FPV 和排障指南都在那里。 + help2: 搜索聊天记录;大多数新手问题此前都已有人解答。 + help3: 提问时请附上 SoC、传感器、你已经尝试过的做法,以及启动日志(如果有)。问题问得好,答案来得快。 + help_title: 怎样提问才会有人回答 + hero_lede: OpenIPC 由每天都在回答问题的人们公开构建。来打个招呼吧。 + hero_title: 来和我们聊聊。 + money_band_html: 更愿意用资金支持?资助这项工作。 + title: 社区 donate: + business_band_html: 如果贵公司依赖 OpenIPC,更可持续的支持方式是建立商业合作关系。看看我们能提供什么。 + crypto_text_html: TON 捐赠可以直接在 Telegram 中通过 @wallet 完成。 + crypto_title: 加密货币 + hero_lede: OpenIPC 使用免费,制作昂贵。捐款用于购买新平台的摄像机、支付维护者的报酬,并维持项目运转。 + hero_title: 资助这项工作。 + oc_button: 在 Open Collective 上捐赠 + oc_text_html: 可定期或一次性捐赠,账目公开透明。定期支持者会被长期列入我们 GitHub 页面的 Sponsors 板块。 + oc_title: Open Collective + thanks: 谢谢你。这比你想象的更有意义。 + title: 捐赠 + where_text: 核心团队成员自掏腰包购买摄像机和 SDK 授权,以扩展硬件支持。资金可以减轻这一负担,为兼职维护者提供补偿,并支付促成硬件合作的展会与厂商会面开销。 + where_title: 钱用在哪里 + donate_band: please_support: 请考虑支持我们 title: 喜欢我们的项目吗? + ecosystem: + contribute_cta_html: 选一个项目,把它做得更好。从这里开始。 + hero_lede: 'OpenIPC 是一个由一百多个开放仓库组成的生态:固件本身、视频管线、周边工具,以及让老芯片继续存活的研究工作。' + hero_title: 不只是固件。 + proj_aviateur: '跨平台接收端:在 Windows、Linux 或 macOS 上观看链路画面。' + proj_burn: 通过串口解救变砖的海思设备。 + proj_coupler: 在原厂固件与 OpenIPC 之间平滑迁移、并可回退的方案。无需焊接,也不需要特殊技能。 + proj_devourer: 基于廉价 Wi-Fi 硬件构建类 SDR 接收端的开放基础。 + proj_divinus: 面向不断增加的平台的开源推流器。 + proj_firmware: 基于 Buildroot 的通用 IP 摄像机固件——在数十个 SoC 系列上取代已被弃用的原厂系统。 + proj_ipctool: 识别几乎任何摄像机的 SoC、传感器和闪存芯片——并在你做任何改动之前备份原厂固件。 + proj_majestic: '固件核心中的推流器:RTSP、ONVIF、WebRTC、HLS、音频、夜视模式。以二进制形式在 Prosperity 许可证下分发,可另行商谈商业条款。' + proj_microbe: 固件的 Web 界面。 + proj_onvif_tt: 'ONVIF 一致性测试工具:验证一台摄像机究竟实现了哪些功能。免费。' + proj_openhisilicon: 让海思原厂 SDK 在主线内核上继续存活——在厂商撒手之后,仍能获得 CVE 修复与现代特性。 + proj_openxiongmai: '面向雄迈 SoC 的同类工作:一套开放的 SDK 替代方案。' + proj_pixelpilot: '接收端:把一块 Rockchip 板子变成带显示输出的专用地面站。' + proj_qemu: 基于 QEMU 的完整海思 IP 摄像机模拟器——桌面上没有硬件也能开发和测试固件。 + proj_rnd_player: '为媒体工程师打造的播放器:以开发者需要的方式检视码流。' + proj_smolrtsp: 可嵌入的 RTSP 1.0 服务端库,适用于摄像机等资源受限设备。支持 TCP 与 UDP,任意负载格式。 + proj_telemetry: 在飞控与地面之间桥接遥测数据。 + proj_waybeam: '链路的发送端:面向 FPV 无人机与 URLLC 设备的灵活视频编码器。' + proj_yamlcli: 一个用于在脚本中编辑 YAML 配置的小型命令行工具。 + section_community_text_html: 社区自制的硬件、支架与配件——例如在 Printables 上标记为 openipc 的可 3D 打印模型。 + section_community_title: 社区项目 + section_core_text: 操作系统、推流器,以及把你从原厂固件带到 OpenIPC 的各种工具。 + section_core_title: 核心固件 + section_longevity_text: '原厂 SDK 默认搭配原厂内核:陈旧、未打补丁、无人维护。我们把它们移植到主线内核——在停产很久之后仍能获得 CVE 修复与现代内核特性。' + section_longevity_title: 长期维护与安全 + section_lowlat_text_html: 面向无人机、机器人与遥操作的开放视频链路。阅读概览。 + section_lowlat_title: 低延迟与 URLLC + section_tools_text: 为摄像机与媒体工程师提供的免费工具——即使你从不刷入我们的固件,它们同样有用。 + section_tools_title: 专业工具 + stage_legend_html: 每个项目都带有诚实的状态标记——与我们用于固件平台的是同一套说法。各阶段是怎么划分的。 + title: 生态 firmware_partitions_calculation: end_address: 结束地址 flash_size_mb: 闪存大小,MB @@ -22,6 +129,28 @@ zh: size_hex: 十六进制大小,字节 start_address: 起始地址 title: 固件分区计算 + get_started: + cta_find_soc: 查找你的 SoC + editions_text: Lite 版适配 8 MB 闪存,覆盖基本功能。Ultimate 版增加了云推流、隧道和更多编解码器等扩展功能,需要 16 MB 闪存。 + editions_title: 两个版本 + hero_lede: 从原厂固件到 OpenIPC,大约半小时——并附上为你的具体芯片生成的指南。 + hero_title: 给你的摄像机第二次生命。 + step1_text_html: 在摄像机上运行 ipctool(通过 telnet 或串口终端),确认 SoC、传感器和闪存芯片。查看标签或搜索 FCC ID 也可以作为备选办法。 + step1_title: 确认你的硬件 + step2_text: 在支持硬件列表中查找你的芯片。向导会直接在浏览器里为你的具体配置组装固件镜像并生成分步安装指南。 + step2_title: 找到你的 SoC + step3_text: 按生成的指南操作。第一步就是完整备份原厂固件——不要跳过。 + step3_title: 先备份,再刷机 + stuck_text_html: 社区每天都在回答问题。到聊天群里提问——请附上你的 SoC、传感器,以及启动日志(如果有)。 + stuck_title: 卡住了? + title: 快速上手 + wall_text_html: 在 Web 界面里打开一个开关,你的摄像机快照就会出现在我们的实时开放墙上——来和社区打个招呼。 + wall_title: 然后加入开放墙 + warning_text_html: 刷机有可能让摄像机变砖。指南会先教你做完整备份,在受支持的型号上 Coupler 也能走更安全的路径。如果没有把握,请在刷机前到聊天群里问一问。 + warning_title: 老实提醒 + webui_title: Web 界面 + what_text: '一套干净的 Linux 系统,搭配 Majestic 推流器:开箱即用的 RTSP、ONVIF、WebRTC 和 HLS,一个 Web 界面,以及完整的 root 权限。你的摄像机,真正属于你。' + what_title: 你会得到什么 green_life: paragraph1: OpenIPC 主要为 IP 摄像机提供开放式固件,直接参与现有硬件的有效利用和再利用,从而为可持续发展做出贡献。 paragraph2: 通过可定制的固件更新和功能,OpenIPC 延长了摄像机的使用寿命,减少了电子垃圾。 @@ -31,6 +160,48 @@ zh: high_resolution_timer: reload_to_reset: 以毫秒为单位显示的时间。重新加载页面以重置计数器。 title: 高分辨率定时器 + home: + business_cta: 与团队沟通 + business_text: 商业支持、定制开发与 OEM 授权——由构建这个平台的人提供。 + business_title: 正在基于 OpenIPC 打造产品? + cta_donate_html: 更愿意直接支持这项工作?捐赠。 + cta_primary: 快速上手 + cta_secondary: 加入我们的 Telegram + cta_title: 这个周末就刷好你的第一台摄像机。 + hero_cta_low_latency: 低延迟视频 + hero_cta_start: 快速上手 + hero_lede: 面向 IP 摄像机、FPV 无人机以及介于两者之间的设备的开放固件与工具。你的硬件应当听你的,而不是听某个厂商云端的。 + hero_microline: 140+ 个仓库 · 数十个 SoC 系列 · 无需云端 + hero_title: 面向嵌入式视频的开放平台。 + partners_title: 制造商、集成商与研究团队的共同选择 + pillar_firmware_text: 面向数十个 SoC 系列的 IP 摄像机开放固件,基于 Buildroot,核心是 Majestic 推流器。 + pillar_firmware_title: 摄像机固件 + pillar_longevity_text: '我们让原厂 SDK 在主线内核上继续存活:在厂商撒手很久之后,依然提供 CVE 修复与现代特性。' + pillar_longevity_title: 长期维护与安全 + pillar_low_latency_text: 面向 FPV 无人机、机器人与遥操作的开放视频链路。在受支持的硬件上,端到端延迟低至约 60 毫秒。 + pillar_low_latency_title: 低延迟视频链路 + pillar_tools_text: ONVIF 一致性测试、摄像机模拟、硬件检测——我们为自己打造并与所有人分享的免费工具。 + pillar_tools_title: 专业工具 + pillars_lede: OpenIPC 不只是固件——它是一个由亲自在自己硬件上运行它的人们构建的生态。 + pillars_title: 一个平台,四条战线 + silicon_title: 支持的芯片厂商 + stats_languages: 种语言 + stats_repos: 个开放仓库 + stats_since: 年起公开开发 + stats_socs: 款支持的 SoC + stats_vendors: 家芯片厂商 + story_cta: 让你的摄像机复活 + story_step1_text: 固件更新停了。安全漏洞还在。App 从应用商店里消失了。 + story_step1_title: 厂商转身离开 + story_step2_text: 服务器被关掉,一台还能用的摄像机就变成了无事可做的塑料和硅片。 + story_step2_title: 云端熄灯 + story_step3_text: 摄像机启动了一套开放且持续维护的系统——并且听你的。没有后门,没有订阅,也少了一件电子垃圾。 + story_step3_title: 你刷入 OpenIPC + story_title: 摄像机不该随着它的云一起死去。 + title: 面向嵌入式视频的开放平台 + wall_cta: "+ 把你的摄像机放这里" + wall_live: '正在直播:世界各地运行 OpenIPC 的摄像机' + wall_snapshot_alt: '图片:来自社区摄像机的实时快照' introduction: alliance: 联盟 button_binaries: 预编译的二进制文件 @@ -57,6 +228,37 @@ zh: integrators: Integrators development: Development title: 介绍 + low_latency: + business_bridge_html: 想基于这条链路做产品?和我们谈谈——我们承接定制开发与 OEM 合作。 + credits_text: 这条无线链路得益于 wfb-ng,整个生态也在 RubyFPV、Mario FPV 等伙伴的参与下不断成长。开源是一项团队运动。 + credits_title: 站在开放的肩膀上 + cta_chat: 加入 FPV 聊天群 + cta_guide: 阅读搭建指南 + fpv_text: '一套完全开放的数字 FPV 系统:没有封闭生态,没有配对绑定的硬件,也没有固件上限。用受支持的摄像机搭建天空端,挑选你的地面站,并可调节管线的每一个环节。' + fpv_title: 'FPV:从镜头到眼镜' + hardware_text_html: 天空端由受支持的摄像机和 AIO 板搭建——请从支持硬件列表开始。RunCam 与 EMAX 出厂即提供基于 OpenIPC 的 FPV 硬件。 + hardware_title: 硬件 + hero_lede: 'FPV 无人机、机器人、遥操作:一套从摄像机到屏幕的开放超低延迟视频方案。在受支持的硬件上,端到端延迟低至约 60 毫秒。' + hero_title: 为等不起的机器提供开放视频链路。 + how_link_text_html: wfb-ng 在普通 Wi-Fi 硬件上广播视频、遥测与控制信号——无需关联,也不会因重传而卡顿。 + how_link_title: 无线链路 + how_rx_text_html: PixelPilot 把一块 Rockchip 板子变成专用地面站,Aviateur 则可在 Windows、Linux 和 macOS 上接收。 + how_rx_title: 接收端——你这一侧 + how_title: 这条链路是怎么工作的 + how_tx_text_html: 一台装有 Waybeam 编码器的 OpenIPC 摄像机负责产生码流并送入无线链路。 + how_tx_title: 发送端——天空这一侧 + latency_1080p30: 1080p30 + latency_1080p60: 1080p60 + latency_720p60: 720p60 + latency_column_config: 配置 + latency_column_g2g: 典型端到端延迟 + latency_intro: '具体数值取决于 SoC、传感器、分辨率、曝光和温度。以下是我们自己实测得到的典型值,而不是最理想情况下的宣传数字:' + latency_note: 使用我们自己的开源硬件延迟测量仪测得——它正是为这件事而造的。 + latency_title: 延迟,实话实说 + robotics_text1: 同一条链路也为工业机器人、AGV、巡检设备和远程操作传输视频——凡是封闭视频系统会把你锁死、或者绕行云端会带来数秒延迟的场合,都用得上。 + robotics_text2: 在受支持的 SoC 上,画面帧会携带与传感器曝光起始对齐的挂钟时间戳——无需额外硬件即可实现多摄像机毫秒级同步。 + robotics_title: '不止于无人机:机器人与遥操作' + title: 低延迟视频 majestic_endpoints: moved_html: '摄像机提供的接口列表现在位于摄像机自身的 Web 界面中,菜单路径为 Majestic → Endpoints。在浏览器中打开 http://<摄像机地址>/,然后从菜单中选择该项。' reason_address: '页面中填入的是您摄像机的真实地址,可以直接把链接复制到播放器;' diff --git a/config/routes.rb b/config/routes.rb index 97cc1bb..6e37059 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,18 @@ root "pages#introduction" get '/aaa', to: 'pages#aaa' + + # The relaunched pages. They answer on their own URLs from here, so they can be + # reviewed and deployed on their own, but nothing links to them yet: the root + # route and the navigation still serve the pre-relaunch structure. The cutover + # is a separate change. + get '/get-started', to: 'pages#get_started' + get '/low-latency', to: 'pages#low_latency' + get '/ecosystem', to: 'pages#ecosystem' + get '/business', to: 'pages#business' + get '/community', to: 'pages#community' + get '/donate', to: 'pages#donate' + get '/home', to: 'pages#home' get '/majestic-endpoints', to: 'pages#majestic_endpoints' get '/coupler', to: redirect('https://github.com/openipc//coupler/') @@ -65,8 +77,15 @@ get '/tools/qr-code-generator', to: 'pages#qr_code_generator' get '/tools/timelaps-interval-calculator', to: 'pages#timelaps_interval_calculator' - # get '/open-wall(/:page)', to: 'snapshots#index' - # get '/open-wall/camera/:id', to: 'snapshots#camera', as: 'openwall_camera' + # Commented out in ed0e025, a bulk tidy-up, while five places that redirect to + # /open-wall were left in: snapshots_controller.rb twice, + # admin/snapshots_controller.rb, and the breadcrumb on three views. Every one + # of them fell through to the catch-all and answered a 302 to the homepage. + # + # This exposes nothing new. `resources :snapshots` has served the same gallery + # at /snapshots throughout; these are the URLs the site itself uses for it. + get '/open-wall/camera/:id', to: 'snapshots#camera', as: 'openwall_camera' + get '/open-wall(/:page)', to: 'snapshots#index', as: 'open_wall' resources :snapshots do get :camera, on: :collection diff --git a/test/controllers/relaunch_pages_test.rb b/test/controllers/relaunch_pages_test.rb new file mode 100644 index 0000000..537a817 --- /dev/null +++ b/test/controllers/relaunch_pages_test.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'test_helper' + +# The seven pages the relaunch adds. They answer on their own URLs but nothing +# links to them yet, so nobody would notice one of them 500ing -- which is +# exactly why they are tested before the cutover rather than after it. +class RelaunchPagesTest < ActionDispatch::IntegrationTest + PAGES = { + '/home' => 'home', + '/get-started' => 'get_started', + '/low-latency' => 'low_latency', + '/ecosystem' => 'ecosystem', + '/business' => 'business', + '/community' => 'community', + '/donate' => 'donate' + }.freeze + + LOCALES = %i[en ru zh].freeze + + PAGES.each do |path, key| + LOCALES.each do |locale| + test "#{path} renders in #{locale}" do + get "#{path}?locale=#{locale}" + + assert_response :success + # A key that exists in en but not here would render as this span, and + # the page would still answer 200. i18n-tasks catches a key missing + # everywhere; only rendering catches one missing from one locale. + assert_no_match(/translation missing/i, response.body) + assert_select 'h1, h2', minimum: 1 + end + end + + test "#{path} sets a page title" do + get path + + assert_select 'title' do |tags| + assert_no_match(/translation missing/i, tags.first.text) + assert_match(/OpenIPC/, tags.first.text) + end + assert_not_nil I18n.t("pages.#{key}.title", default: nil), "pages.#{key}.title is not defined" + end + end + + # A fresh checkout has an empty database. The homepage reads counts and + # snapshots from it, and must render rather than 500 or show "0 supported + # SoCs" as though that were a fact about the project. + test 'the homepage renders against an empty database' do + assert_equal 0, Snapshot.count + assert_equal 0, Soc.count + + get '/home' + + assert_response :success + end + + # These pages lay out their own full-bleed sections and opt out of the + # layout's wrapper. If the switch stopped working they would still render, + # just wrongly, inside a centred column. + # + # The selector is the layout's own `container mb-4`, not any `.container`: + # most of these pages open a plain `.container` of their own directly under + #
, and matching that would pass whether the switch worked or not. + test 'the relaunch pages opt out of the layout wrapper' do + PAGES.each_key do |path| + get path + + assert_empty css_select('main > div.container.mb-4'), "#{path} kept the layout wrapper" + end + end + + # The inverse, so the switch is pinned from both sides: a page that asks for + # nothing must still get the wrapper. + test 'a pre-relaunch page still gets the layout wrapper' do + get '/introduction' + + assert_not_empty css_select('main > div.container.mb-4') + end + + # Every internal link on the new pages has to resolve. A link to a path the + # router does not know falls through the catch-all to a 302 home, which looks + # like a working link right up until someone clicks it. + test 'internal links on the relaunch pages all resolve' do + PAGES.each_key do |path| + get path + + hrefs = css_select('a[href^="/"]').map { |a| a['href'] }.uniq + hrefs.each do |href| + get href + # A redirect is fine -- /supported-hardware legitimately 301s to + # /featured. What is not fine is landing at the homepage, which is + # where the "*unmatched" catch-all sends anything the router does not + # recognise: the signature of a link to a path that does not exist. + if response.redirect? + assert_not_equal '/', URI.parse(response.location).path, + "#{path} links to #{href}, which falls through to the catch-all" + follow_redirect! + end + + assert_response :success, "#{path} links to #{href}, which answered #{response.status}" + end + end + end + + # The integrator wall is territory-specific: these companies serve Russia and + # were explicitly not to be shown to everyone. + test 'Russian integrators appear for ru and for nobody else' do + marker = PagesHelper::RU_INTEGRATORS.first[:img] + + get '/home?locale=ru' + + assert_includes response.body, marker.sub('.png', '') + + %w[en zh].each do |locale| + get "/home?locale=#{locale}" + + assert_not_includes response.body, marker.sub('.png', ''), + "RU integrators leaked into #{locale}" + end + end + + test 'every partner logo the helper names exists as an asset' do + (PagesHelper::INTERNATIONAL_PARTNERS + PagesHelper::RU_INTEGRATORS).each do |logo| + assert_path_exists Rails.root.join('app/assets/images', logo[:img]) + end + end +end