diff --git a/app/assets/images/partners/faceter_mini.png b/app/assets/images/partners/faceter_mini.png new file mode 100644 index 00000000..a9b938bc Binary files /dev/null and b/app/assets/images/partners/faceter_mini.png differ diff --git a/app/assets/stylesheets/components/_cards.scss b/app/assets/stylesheets/components/_cards.scss index d985f200..ae29f2fc 100644 --- a/app/assets/stylesheets/components/_cards.scss +++ b/app/assets/stylesheets/components/_cards.scss @@ -18,6 +18,14 @@ } } +// A link inside the card body sits under .stretched-link's overlay and cannot +// be clicked. Lifting it above restores it without giving up the whole-card +// click target -- which is what the arrow at the bottom is for. +.pillar-card .card-body p a { + position: relative; + z-index: 2; +} + .pillar-card .icon-chip { align-items: center; background: rgba($indigo, .08); diff --git a/app/helpers/pages_helper.rb b/app/helpers/pages_helper.rb index 2b20578c..d0ab84f7 100644 --- a/app/helpers/pages_helper.rb +++ b/app/helpers/pages_helper.rb @@ -2,47 +2,70 @@ # 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. + # The partner wall, grouped, because "partners" was doing too much work as one + # word. A chip vendor that ships OpenIPC out of the box, a company that + # installs cameras for a living, a university team and the host of our + # finances are all on the wall for different reasons, and a page usually wants + # some of those reasons rather than all of them: /business shows who builds + # and integrates hardware, and nobody else. # - # 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. + # Entries commented out here are commented out on the old /introduction wall + # too, with the same URLs, so nothing appears or disappears silently. + # Uncommenting a line 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 + # partners/baresip_mini.png exists as an asset but has never been on the wall. + # Left off rather than introduced by a refactor. + PARTNER_GROUPS = { + # Who hosts our money and our code. + global: [ + { 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' } + ], + # Ships hardware with OpenIPC on it. + manufacturers: [ + { name: 'RunCam', url: 'https://runcam.com/', img: 'partners/runcam_mini.png' }, + { name: 'CCDCAM', url: 'https://ccdcam.com/', img: 'partners/ccdcam_mini.png' } + # Commented out on /introduction, so commented out here: + # { name: 'EMAX', url: 'https://emaxmodel.com/', img: 'partners/emax_mini.png' }, + ], + # Builds systems on it for other people. RU_INTEGRATORS is appended to this + # group for Russian-speaking visitors only -- see partner_groups. + integrators: [ + { name: 'GoodCam', url: 'https://www.goodcam.io/', img: 'partners/goodcam_mini.png' }, + { name: 'AnyCam', url: 'https://anycam.io/', img: 'partners/anycam_mini.png' }, + { name: 'Faceter', url: 'https://faceter.cam/', img: 'partners/faceter_mini.png' }, + { name: 'Really', url: 'https://opencollective.com/really-541ee976', img: 'partners/really_mini.png' } + ], + # The FPV projects we grew up alongside. + fpv: [ + { 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' } + ], + # Student and university teams flying or teaching on OpenIPC. + education: [ + { name: 'TUDSaT', url: 'https://www.tudsat.space/', img: 'partners/tudsat_mini.png' }, + { name: 'WuSpace', url: 'https://wuespace.de/', img: 'partners/wuespace_mini.png' } + ], + # Kept in the system, rendered nowhere. There is no page for trade shows and + # exhibitions yet; when there is, it asks for :exhibitions and both the logo + # and the link are already here. Deliberately absent from HOME_PARTNER_ROWS + # and from every partner_groups call -- a test holds it that way. + exhibitions: [ + { name: 'Expo Electronica', url: 'https://expoelectronica.ru/en/', img: 'partners/expo-electronica_mini.png' } + ], + # Reverse engineering and silicon research we build on. + research: [ + { name: 'Linux Chenxing', url: 'https://linux-chenxing.org/', img: 'partners/linuxchenxing_mini.png' } + ] + }.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. + # wanted, and the reverse -- gating them in CSS with `html:not([lang="ru"])` + # -- never worked, because no logo ever carried the `ru` class the rule + # selects on. 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' }, @@ -51,7 +74,6 @@ module PagesHelper { 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' }, @@ -68,7 +90,6 @@ module PagesHelper # { 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 @@ -104,13 +125,60 @@ def latency_bar_style(path) "left: #{left.round(1)}%; width: #{width.round(1)}%" end + # The Russian integrator list is appended here and nowhere else, so every + # caller gets the same territory rule whether it asks for a group or a row. + def logos_in(key) + logos = PARTNER_GROUPS.fetch(key) + logos += RU_INTEGRATORS if key == :integrators && I18n.locale.eql?(:ru) + logos + end + def page_title [@page_title, 'OpenIPC'].join(' - ') end - def partner_logos - logos = INTERNATIONAL_PARTNERS - logos += RU_INTEGRATORS if I18n.locale.eql?(:ru) - logos + # How the homepage arranges the six groups. At six rows half of them were a + # single logo under a heading, which reads as a gap rather than a group -- + # research is one logo, and integrators is one until the visitor is Russian. + # Three rows, in the order the wall is meant to be read: who ships and + # installs the hardware, who hosts us, and who we work alongside. + # + # /business still asks for its two groups directly. Splitting rows out again + # is a line here, not a rewrite. + # Order matters twice over: the rows appear in the order written here, and + # within a row the groups are laid out in the order they are listed. + HOME_PARTNER_ROWS = { + global: %i[global], + trade: %i[manufacturers integrators], + friends: %i[research fpv education] + }.freeze + + # Rows of [label, logos], in the order HOME_PARTNER_ROWS declares. A row whose + # groups are all empty is dropped rather than rendered as a heading over + # nothing -- which is what a territory-specific group looks like in a locale + # that has no entries for it yet. + # + # Note this does not go through partner_logos: there, no arguments means + # every group, so a row that named no groups would quietly render all of them. + def partner_rows(rows) + rows.filter_map do |label, keys| + logos = keys.flat_map { |key| logos_in(key) } + [label, logos] if logos.any? + end + end + + # The named groups, in the order asked for, each as [key, logos]. Groups that + # would render empty are dropped rather than left as a heading over nothing. + # With no arguments, every group. + def partner_groups(*keys) + keys = PARTNER_GROUPS.keys if keys.empty? + keys.filter_map do |key| + logos = logos_in(key) + [key, logos] if logos.any? + end + end + + def partner_logos(*keys) + partner_groups(*keys).flat_map(&:last) end end diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb index 6f88a264..0f255e0a 100644 --- a/app/views/layouts/_footer.html.erb +++ b/app/views/layouts/_footer.html.erb @@ -36,7 +36,7 @@
<%= t('.accounting') %>
<%= t('.crypto_text_html') %>
-<%= t('.pillars_lede') %>
<%= t('.credits_text') %>
+<%= t('.credits_text_html') %>
diff --git a/app/views/shared/_logo_wall.html.erb b/app/views/shared/_logo_wall.html.erb index e4eddba7..1b4f6e48 100644 --- a/app/views/shared/_logo_wall.html.erb +++ b/app/views/shared/_logo_wall.html.erb @@ -1,5 +1,9 @@ -<%# Partner/integrator logo wall. Locals: logos (array of {name:, url:, img:}). %> -
<%= t('.intro_html') %>
diff --git a/config/locales/en.yml b/config/locales/en.yml index 03ab583a..73ef22e5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -70,7 +70,7 @@ en: attention: Attention! backup: info: OpenIPC U-Boot overwrites the original crypto partition making it impossible to revert to the factory firmware unless you have a full flash backup for this particular camera! To create a full firmware backup start a TFTP server and run these commands in bootloader shell. - more_info_html: Please refer to Installation instructions in the project's wiki for more information. + more_info_html: Please refer to Installation instructions in the project's wiki for more information. subtitle: Back up your stock firmware, don't skip this part of the installation process! title: Save the original firmware backup_32: ATTENTION! Before backing up 32M flash ROM, make sure that the camera has at least 64M of RAM. @@ -107,7 +107,6 @@ en: info: If you need to restore the original firmware, use the backup file you have created using the commands above. Place the backup file into the root directory of your TFTP server and run these commands in bootloader shell. title: Restore camera stock firmware version: - fpv: FPV lite: Lite nand: NAND neo: Neo @@ -177,6 +176,12 @@ en: nosd: Camera does not have an SD card slot sd: Camera has an SD card slot site: + partners: + friends: Friendly projects + global: Global partners + integrators: Integrators + manufacturers: Manufacturers + trade: Manufacturers and integrators default_meta_description: 'OpenIPC is the open platform for embedded video: open-source firmware for IP cameras, ultra-low-latency video links for drones and robots, and free tools for camera engineers.' disclaimer: The information contained on this site is for general informational purposes only and is not intended to be relied upon by the visitor. All software and firmware available on this site is provided AS IS and for research purposes only. OpenIPC shall not be liable for any loss or damage caused by the use of these files or the use of, or reliance upon, any information contained in this site. snapshot: @@ -188,7 +193,7 @@ en: icon: snapshot_alt: 'Image: Snapshot' index: - intro_html: Cameras send these themselves — nobody goes out and collects them. Each image is removed a couple of days after it arrives. Want yours here? Turn on the Open Wall option in your camera's web interface. + intro_html: Cameras send these themselves — nobody goes out and collects them. Want yours here? Turn on the Open Wall option in your camera's web interface. no_signal: No signal snapshot_alt: 'Image: Snapshot' stay_tuned: stay tuned diff --git a/config/locales/pages.en.yml b/config/locales/pages.en.yml index f3f79af5..269ebf0f 100644 --- a/config/locales/pages.en.yml +++ b/config/locales/pages.en.yml @@ -67,8 +67,6 @@ en: 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 accounting: We're committed to open and transparent accounting, and are grateful for any support you can provide to our efforts to advance OpenIPC and our mission toward a sustainable future. 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. @@ -99,7 +97,6 @@ en: 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. @@ -167,10 +164,10 @@ en: 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_microline: 140+ repositories · dozens of SoC families · since 2019 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_text_html: 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 @@ -181,7 +178,6 @@ en: 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 @@ -190,7 +186,7 @@ en: 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_step2_title: The servers go quiet 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. @@ -200,13 +196,13 @@ en: wall_snapshot_alt: 'Image: live snapshot from a community camera' 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 devourer, and the ecosystem grows with friends like RubyFPV and Mario FPV. Open source is a team sport. + credits_text_html: The radio link exists thanks to wfb-ng and devourer, 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_text_html: Air units are built from supported cameras and AIO boards — start from the supported hardware list. RunCam and other vendors 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 30 ms on supported hardware.' hero_title: Open video links for machines that can't wait. diff --git a/config/locales/pages.ru.yml b/config/locales/pages.ru.yml index a9bb32c6..9abe867e 100644 --- a/config/locales/pages.ru.yml +++ b/config/locales/pages.ru.yml @@ -67,8 +67,6 @@ ru: title: Сообщество donate: business_band_html: Если ваша компания зависит от OpenIPC, самый устойчивый способ поддержать проект — коммерческое сотрудничество. Что мы предлагаем. - crypto_text_html: Пожертвования в TON работают прямо в Telegram через @wallet. - crypto_title: Криптовалюта accounting: Мы стремимся к открытому и прозрачному учету и благодарны за любую поддержку, которую вы можете оказать нашим усилиям по продвижению OpenIPC и нашей миссии по обеспечению устойчивого будущего. hero_lede: OpenIPC бесплатен в использовании и дорог в производстве. Пожертвования покупают камеры для новых платформ, оплачивают работу мейнтейнеров и держат свет включённым. hero_title: Поддержите работу. @@ -99,7 +97,6 @@ ru: 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. @@ -167,10 +164,10 @@ ru: hero_cta_low_latency: Видео с низкой задержкой hero_cta_start: Начать hero_lede: Открытая прошивка и инструменты для IP-камер, FPV-дронов и всего, что между ними. Ваше железо должно подчиняться вам, а не облаку производителя. - hero_microline: 140+ репозиториев · десятки семейств SoC · облако не требуется + hero_microline: 140+ репозиториев · десятки семейств SoC · с 2019 года hero_title: Открытая платформа для встраиваемого видео. partners_title: Нам доверяют производители, интеграторы и исследователи - pillar_firmware_text: Открытая ОС на базе Buildroot для IP-камер на десятках семейств SoC, со стримером Majestic в основе. + pillar_firmware_text_html: Открытая ОС на базе Buildroot для IP-камер на десятках семейств SoC, со стримером Majestic в основе. pillar_firmware_title: Прошивка для камер pillar_longevity_text: 'Мы поддерживаем вендорские SDK на mainline-ядрах: исправления CVE и новые возможности спустя годы после ухода производителя.' pillar_longevity_title: Долголетие и безопасность @@ -181,7 +178,6 @@ ru: pillars_lede: OpenIPC — больше, чем прошивка. Это экосистема, которую делают люди, использующие её на собственном железе. pillars_title: Одна платформа, четыре направления silicon_title: Работает на чипах - stats_languages: языков stats_repos: открытых репозиториев stats_since: открытая разработка с stats_socs: поддерживаемых SoC @@ -190,7 +186,7 @@ ru: story_step1_text: Обновления прекращаются. Дыры в безопасности остаются. Приложение исчезает из магазина. story_step1_title: Производитель уходит story_step2_text: Серверы выключают, и исправная камера превращается в кусок пластика и кремния без дела. - story_step2_title: Облако гаснет + story_step2_title: Серверы замолкают story_step3_text: Камера загружает открытую, поддерживаемую систему — и подчиняется вам. Без бэкдоров. Без подписок. Меньше электронного мусора. story_step3_title: Вы прошиваете OpenIPC story_title: Камера не должна умирать вместе со своим облаком. @@ -200,13 +196,13 @@ ru: wall_snapshot_alt: 'Изображение: живой снимок с камеры сообщества' low_latency: business_bridge_html: Строите продукт на этом линке? Напишите нам — мы делаем заказную разработку и OEM. - credits_text: Радиолинк существует благодаря wfb-ng и devourer, а экосистема растёт вместе с друзьями — RubyFPV и Mario FPV. Open source — командный спорт. + credits_text_html: Радиолинк существует благодаря wfb-ng и devourer, а экосистема растёт вместе с друзьями — 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_text_html: Борт собирается из поддерживаемых камер и AIO-плат — начните со списка поддерживаемого оборудования. RunCam и другие вендоры выпускают FPV-железо на OpenIPC из коробки. hardware_title: Железо hero_lede: 'FPV-дроны, роботы, телеуправление: открытый видеотракт со сверхнизкой задержкой от камеры до экрана. От ~30 мс «от стекла до стекла» на поддерживаемом железе.' hero_title: Открытые видеолинки для машин, которые не умеют ждать. diff --git a/config/locales/pages.zh.yml b/config/locales/pages.zh.yml index cfe4fb99..622a4cc5 100644 --- a/config/locales/pages.zh.yml +++ b/config/locales/pages.zh.yml @@ -67,8 +67,6 @@ zh: title: 社区 donate: business_band_html: 如果贵公司依赖 OpenIPC,更可持续的支持方式是建立商业合作关系。看看我们能提供什么。 - crypto_text_html: TON 捐赠可以直接在 Telegram 中通过 @wallet 完成。 - crypto_title: 加密货币 accounting: 我们致力于共建一个开放和透明的数字环境,并由衷地感谢你对我们的努力提供的任何支持,以推动OpenIPC和我们的使命走向一个可持续的未来。 hero_lede: OpenIPC 使用免费,制作昂贵。捐款用于购买新平台的摄像机、支付维护者的报酬,并维持项目运转。 hero_title: 资助这项工作。 @@ -99,7 +97,6 @@ zh: 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 打印模型。 @@ -167,10 +164,10 @@ zh: hero_cta_low_latency: 低延迟视频 hero_cta_start: 快速上手 hero_lede: 面向 IP 摄像机、FPV 无人机以及介于两者之间的设备的开放固件与工具。你的硬件应当听你的,而不是听某个厂商云端的。 - hero_microline: 140+ 个仓库 · 数十个 SoC 系列 · 无需云端 + hero_microline: 140+ 个仓库 · 数十个 SoC 系列 · 自 2019 年 hero_title: 面向嵌入式视频的开放平台。 partners_title: 制造商、集成商与研究团队的共同选择 - pillar_firmware_text: 面向数十个 SoC 系列的 IP 摄像机开放固件,基于 Buildroot,核心是 Majestic 推流器。 + pillar_firmware_text_html: 面向数十个 SoC 系列的 IP 摄像机开放固件,基于 Buildroot,核心是 Majestic 推流器。 pillar_firmware_title: 摄像机固件 pillar_longevity_text: '我们让原厂 SDK 在主线内核上继续存活:在厂商撒手很久之后,依然提供 CVE 修复与现代特性。' pillar_longevity_title: 长期维护与安全 @@ -181,7 +178,6 @@ zh: pillars_lede: OpenIPC 不只是固件——它是一个由亲自在自己硬件上运行它的人们构建的生态。 pillars_title: 一个平台,四条战线 silicon_title: 支持的芯片厂商 - stats_languages: 种语言 stats_repos: 个开放仓库 stats_since: 年起公开开发 stats_socs: 款支持的 SoC @@ -190,7 +186,7 @@ zh: story_step1_text: 固件更新停了。安全漏洞还在。App 从应用商店里消失了。 story_step1_title: 厂商转身离开 story_step2_text: 服务器被关掉,一台还能用的摄像机就变成了无事可做的塑料和硅片。 - story_step2_title: 云端熄灯 + story_step2_title: 服务器悄然关停 story_step3_text: 摄像机启动了一套开放且持续维护的系统——并且听你的。没有后门,没有订阅,也少了一件电子垃圾。 story_step3_title: 你刷入 OpenIPC story_title: 摄像机不该随着它的云一起死去。 @@ -200,13 +196,13 @@ zh: wall_snapshot_alt: '图片:来自社区摄像机的实时快照' low_latency: business_bridge_html: 想基于这条链路做产品?和我们谈谈——我们承接定制开发与 OEM 合作。 - credits_text: 这条无线链路得益于 wfb-ng 与 devourer,整个生态也在 RubyFPV、Mario FPV 等伙伴的参与下不断成长。开源是一项团队运动。 + credits_text_html: 这条无线链路得益于 wfb-ng 与 devourer,整个生态也在 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_text_html: 天空端由受支持的摄像机和 AIO 板搭建——请从支持硬件列表开始。RunCam 等厂商出厂即提供基于 OpenIPC 的 FPV 硬件。 hardware_title: 硬件 hero_lede: 'FPV 无人机、机器人、遥操作:一套从摄像机到屏幕的开放超低延迟视频方案。在受支持的硬件上,端到端延迟低至约 30 毫秒。' hero_title: 为等不起的机器提供开放视频链路。 diff --git a/config/locales/ru.yml b/config/locales/ru.yml index a9aa1614..3c10dc20 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -72,7 +72,7 @@ ru: attention: Внимание! backup: info: OpenIPC U-Boot перезаписывает исходный криптографический раздел, делая невозможным возврат к заводской прошивке, если у вас нет полной резервной копии флэш-памяти для этой конкретной камеры! Чтобы создать полную резервную копию прошивки, запустите TFTP-сервер и выполните эти команды в оболочке загрузчика. - more_info_html: Пожалуйста, обратитесь к инструкциям по установке в вики проекта для получения дополнительной информации. + more_info_html: Пожалуйста, обратитесь к инструкциям по установке в вики проекта для получения дополнительной информации. subtitle: Сделайте резервную копию стоковой прошивки, не пропускайте эту часть процесса установки! title: Сохраните оригинальную прошивку backup_32: ВНИМАНИЕ! Перед резервным копированием 32-мегабайтной флэш-памяти убедитесь, что камера имеет не менее 64 МБ оперативной памяти. @@ -109,7 +109,6 @@ ru: info: Если вам нужно восстановить исходную прошивку, используйте файл резервной копии, который вы создали с помощью приведенных выше команд. Поместите файл резервной копии в корневой каталог вашего TFTP-сервера и выполните эти команды в оболочке загрузчика. title: Восстановить стоковую прошивку камеры version: - fpv: FPV lite: Lite nand: NAND neo: Neo @@ -179,6 +178,12 @@ ru: nosd: Камера не имеет слота для SD-карты sd: Камера имеет слот для SD-карты site: + partners: + friends: Дружественные проекты + global: Глобальные партнёры + integrators: Интеграторы + manufacturers: Производители + trade: Производители и интеграторы default_meta_description: 'OpenIPC — открытая платформа для встраиваемого видео: открытая прошивка для IP-камер, видеолинки со сверхнизкой задержкой для дронов и роботов, бесплатные инструменты для инженеров.' disclaimer: Информация, содержащаяся на этом сайте, предназначена только для общих информационных целей и не предназначена для того, чтобы посетитель полагался исключительно на нее. Все программное обеспечение и прошивки, доступные на этом сайте, предоставляются КАК ЕСТЬ и только для исследовательских целей. OpenIPC не несет ответственности за какие-либо убытки или ущерб, вызванные использованием этих файлов или не использованием их или безусловным доверием к любой информации, содержащейся на этом сайте. snapshot: @@ -190,7 +195,7 @@ ru: icon: snapshot_alt: 'Изображение: снимок' index: - intro_html: Камеры присылают снимки сами — никто их не собирает. Каждый снимок удаляется через пару дней после загрузки. Хотите, чтобы здесь был ваш? Включите опцию Open Wall в веб-интерфейсе камеры. + intro_html: Камеры присылают снимки сами — никто их не собирает. Хотите, чтобы здесь был ваш? Включите опцию Open Wall в веб-интерфейсе камеры. no_signal: Нет сигнала snapshot_alt: 'Изображение: снимок' stay_tuned: Следите за обновлениями diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 2a1294b9..73cc1b0d 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -70,7 +70,7 @@ zh: attention: 注意! backup: info: OpenIPC U-Boot 会覆盖原始的加密分区,因此无法恢复为出厂固件,除非您拥有此特定摄像机的完整闪存备份!要创建完整的固件备份,请启动 TFTP 服务器并在引导加载程序 shell 中执行这些命令。 - more_info_html: 有关详细信息,请参阅项目 wiki 中的安装说明。 + more_info_html: 有关详细信息,请参阅项目 wiki 中的安装说明。 subtitle: 备份您的原始固件,请不要跳过这个步骤! title: 保存原始固件 backup_32: 请注意!在备份 32M 闪存之前,请确保摄像机至少有 64M 的 RAM。 @@ -107,7 +107,6 @@ zh: info: 如果您需要恢复原始固件,请使用您使用上述命令创建的备份文件。将备份文件放入 TFTP 服务器的根目录,并在引导加载程序 shell 中执行这些命令。 title: 恢复摄像机原始固件 version: - fpv: FPV lite: Lite nand: Nand neo: Neo @@ -177,6 +176,12 @@ zh: nosd: 摄像机没有 SD 卡槽 sd: 摄像机有一个 SD 卡槽 site: + partners: + friends: 友好项目 + global: 全球合作伙伴 + integrators: 集成商 + manufacturers: 硬件厂商 + trade: 厂商与集成商 default_meta_description: 'OpenIPC 是面向嵌入式视频的开放平台:适用于 IP 摄像机的开源固件、面向无人机与机器人的超低延迟视频链路,以及为摄像机工程师提供的免费工具。' disclaimer: 本网站包含的信息仅供一般参考之用,访问者不应依赖这些信息。本网站上提供的所有软件和固件均按原样提供,仅供研究使用。对于因使用这些文件或依赖本网站中包含的任何信息而造成的任何损失或损害,OpenIPC 概不负责。 snapshot: @@ -188,7 +193,7 @@ zh: icon: snapshot_alt: 图片:快照 index: - intro_html: 这些图片是摄像机自己上传的——没有人去采集。每张图片会在上传几天后被删除。想让你的画面出现在这里?在摄像机的 Web 界面中开启 Open Wall 选项即可。 + intro_html: 这些图片是摄像机自己上传的——没有人去采集。想让你的画面出现在这里?在摄像机的 Web 界面中开启 Open Wall 选项即可。 no_signal: 没信号 snapshot_alt: 图片:快照 stay_tuned: 敬请关注 diff --git a/config/routes.rb b/config/routes.rb index 653d89d2..4f245a06 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,14 +42,13 @@ } get '/majestic-endpoints', to: 'pages#majestic_endpoints' - get '/coupler', to: redirect('https://github.com/openipc//coupler/') - get '/firmware', to: redirect('https://github.com/openipc//firmware/') - get '/ipctool', to: redirect('https://github.com/openipc/ipctool/') - get '/microbe-web', to: redirect('https://github.com/openipc/microbe-web/') - get '/smolrtsp', to: redirect('https://github.com/openipc/smolrtsp/') - get '/telemetry', to: redirect('https://github.com/openipc/telemetry/') - get '/yaml-cli', to: redirect('https://github.com/openipc/yaml-cli/') - get '/wiki', to: redirect('https://github.com/openipc/wiki/') + get '/coupler', to: redirect('https://github.com/OpenIPC/coupler/') + get '/firmware', to: redirect('https://github.com/OpenIPC/firmware/') + get '/ipctool', to: redirect('https://github.com/OpenIPC/ipctool/') + get '/microbe-web', to: redirect('https://github.com/OpenIPC/microbe-web/') + get '/smolrtsp', to: redirect('https://github.com/OpenIPC/smolrtsp/') + get '/yaml-cli', to: redirect('https://github.com/OpenIPC/yaml-cli/') + get '/wiki', to: redirect('https://github.com/OpenIPC/wiki/') get '/hardware', to: redirect('/supported-hardware/featured') get '/ru/installation.md', to: redirect('https://github.com/OpenIPC/wiki/blob/master/ru/installation.md') @@ -64,19 +63,21 @@ get '/install_switcam_hs303', to: redirect('https://github.com/OpenIPC/wiki/blob/master/ru/hardware-hs303.md') # FIXME: combine with above - get '/coupler(/*any)', to: redirect('https://github.com/openipc//coupler') - get '/firmware(/*any)', to: redirect('https://github.com/openipc/firmware') - get '/ipctool(/*any)', to: redirect('https://github.com/openipc/ipctool') - get '/microbe-web(/*any)', to: redirect('https://github.com/openipc/microbe-web') - get '/smolrtsp(/*any)', to: redirect('https://github.com/openipc/smolrtsp') - get '/telemetry(/*any)', to: redirect('https://github.com/openipc/telemetry') - get '/yaml-cli(/*any)', to: redirect('https://github.com/openipc/yaml-cli') - get '/wiki(/*any)', to: redirect('https://github.com/openipc/wiki') + get '/coupler(/*any)', to: redirect('https://github.com/OpenIPC/coupler') + get '/firmware(/*any)', to: redirect('https://github.com/OpenIPC/firmware') + get '/ipctool(/*any)', to: redirect('https://github.com/OpenIPC/ipctool') + get '/microbe-web(/*any)', to: redirect('https://github.com/OpenIPC/microbe-web') + get '/smolrtsp(/*any)', to: redirect('https://github.com/OpenIPC/smolrtsp') + get '/yaml-cli(/*any)', to: redirect('https://github.com/OpenIPC/yaml-cli') + get '/wiki(/*any)', to: redirect('https://github.com/OpenIPC/wiki') get '/SDK', to: redirect('/supported-hardware') get '/sponsor', to: redirect('/donate') get '/green_life', to:'pages#green_life' + # Not linked from anywhere while there is nothing to sell -- see the footer. + # The route and the page stay: the shop is expected back, plausibly through + # Open Collective, and deleting them would mean writing it all again. get '/merchandise', to: 'pages#merchandise' get '/our-team', to: 'pages#our_team' get '/stages-of-firmware-development', to: 'pages#stages_of_firmware_development' @@ -146,6 +147,14 @@ match "/binaries", to: proc { [410, { "Content-Type" => "text/plain" }, ["Gone\n"]] }, via: :all, as: :retired_binaries + # Same treatment, same reason. /telemetry was a shortcut to + # github.com/OpenIPC/telemetry, which does not exist and by all appearances + # never has, so it had been bouncing visitors to GitHub's own 404. Deleting + # the route is worse rather than better: the catch-all answers unknown paths + # with a 302 to the homepage, which tells a crawler the page moved there. + match "/telemetry(/*any)", to: proc { [410, { "Content-Type" => "text/plain" }, ["Gone\n"]] }, + via: :all, as: :retired_telemetry + match "*unmatched", to: "application#route_not_found", constraints: lambda { |req| req.path.exclude? 'rails/active_storage' }, via: :all diff --git a/test/controllers/redirects_test.rb b/test/controllers/redirects_test.rb index 4d35159e..ba9195ab 100644 --- a/test/controllers/redirects_test.rb +++ b/test/controllers/redirects_test.rb @@ -79,4 +79,34 @@ class RedirectsTest < ActionDispatch::IntegrationTest assert_response :success end + + # These send visitors off to GitHub, so a typo here is a dead end nobody on + # this side sees. One of them spelled the organisation `openipc//coupler`, + # with the wrong case and a doubled slash, and `/telemetry` pointed at a + # repository that has never existed. + GITHUB_REDIRECTS = %w[coupler firmware ipctool microbe-web smolrtsp yaml-cli wiki].freeze + + test 'every GitHub shortcut points at a well-formed OpenIPC repository' do + GITHUB_REDIRECTS.each do |repo| + ['', '/some/deep/path'].each do |suffix| + get "/#{repo}#{suffix}" + + assert_response :redirect, "/#{repo}#{suffix} does not redirect" + assert_match %r{\Ahttps://github\.com/OpenIPC/#{Regexp.escape(repo)}/?\z}, + response.location, + "/#{repo}#{suffix} sends visitors to #{response.location}" + end + end + end + + # The repository behind it does not exist, so bouncing anyone to GitHub's own + # 404 was worse than answering here. Deleting the route would be worse still: + # unknown paths 302 to the homepage, which claims the page moved there. + test '/telemetry says it is gone rather than pretending it moved' do + ['/telemetry', '/telemetry/anything'].each do |path| + get path + + assert_response :gone, "#{path} answered #{response.status}" + end + end end diff --git a/test/controllers/relaunch_pages_test.rb b/test/controllers/relaunch_pages_test.rb index f2582e89..b3bb700e 100644 --- a/test/controllers/relaunch_pages_test.rb +++ b/test/controllers/relaunch_pages_test.rb @@ -149,12 +149,158 @@ class RelaunchPagesTest < ActionDispatch::IntegrationTest end end + # /ecosystem is a list of our own projects, so every card links into the + # OpenIPC organisation. qemu-hisilicon was still pointing at the personal + # account it was developed in, and telemetry was a card for a repository that + # has never existed. The selector is the card's own link, not every GitHub + # URL on the page: the prose links the wiki too, and that is not a project. + test 'every ecosystem project links a repository under OpenIPC' do + get '/ecosystem' + + repos = css_select('.project-card a[href*="github.com"]').map { |a| a['href'] } + + assert_not_empty repos + repos.each do |href| + assert_match %r{\Ahttps://github\.com/OpenIPC/[\w.-]+\z}, href, + "#{href} is not a repository under the OpenIPC organisation" + end + end + test 'every partner logo the helper names exists as an asset' do - (PagesHelper::INTERNATIONAL_PARTNERS + PagesHelper::RU_INTEGRATORS).each do |logo| + logos = PagesHelper::PARTNER_GROUPS.values.flatten + PagesHelper::RU_INTEGRATORS + + assert_operator logos.size, :>, 10 + logos.each do |logo| assert_path_exists Rails.root.join('app/assets/images', logo[:img]) end end + # A commercial reader is asking who ships hardware and who installs it. Our + # code host, our FPV friends and our university teams are on the homepage + # wall for good reasons, none of which are an answer to that question. + test '/business shows only manufacturers and integrators' do + get '/business' + + shown = PagesHelper.const_get(:PARTNER_GROUPS) + %i[manufacturers integrators].each do |key| + shown[key].each { |l| assert_includes response.body, l[:img].sub('.png', '') } + assert_includes response.body, I18n.t("site.partners.#{key}") + end + %i[global fpv education research].each do |key| + shown[key].each do |logo| + assert_not_includes response.body, logo[:img].sub('.png', ''), + "#{logo[:name]} is on /business, which is not the audience for it" + end + end + end + + # Three labelled rows rather than one undifferentiated block of logos -- and + # rather than six, half of which were a single logo under a heading. + test 'the homepage groups the wall instead of pouring it into one row' do + get '/' + + rows = css_select('.logo-wall').size + labels = PagesHelper::HOME_PARTNER_ROWS.keys + + assert_equal labels.size, rows + # In the declared order, not merely present. + shown = css_select('h3.text-uppercase').map { |h| h.text.strip } + + assert_equal labels.map { |l| I18n.t("site.partners.#{l}") }, shown + + # And within a row, the groups are laid out in the order they are listed. + PagesHelper::HOME_PARTNER_ROWS.each_value do |keys| + next if keys.size < 2 + + positions = keys.map { |k| response.body.index(PagesHelper::PARTNER_GROUPS.fetch(k).first[:img].sub('.png', '')) } + + assert_equal positions.sort, positions, "#{keys.join(', ')} are out of order" + end + # Every group the rows name still reaches the page through one of them. + PagesHelper::HOME_PARTNER_ROWS.each_value do |keys| + keys.each do |key| + PagesHelper::PARTNER_GROUPS.fetch(key).each do |logo| + assert_includes response.body, logo[:img].sub('.png', '') + end + end + end + end + + # A group with nothing in it must take its heading down with it. Territory + # gating means a group can be empty in one locale and not another, and a bare + # heading over no logos reads as a broken page rather than an empty category. + test 'a row with nothing in it renders neither logos nor a heading' do + get '/' + + # partner_rows needs no view context, so it can be exercised directly. + helpers = Object.new.extend(PagesHelper) + + assert_empty helpers.partner_rows(empty: []) + assert_equal %i[global], helpers.partner_rows(global: %i[global]).map(&:first) + # Every heading on the page has a wall under it. + assert_equal css_select('.logo-wall').size, css_select('h3.text-uppercase').size + end + + # /business asks for its two groups by name, each its own labelled row. + test '/business lists manufacturers first, then integrators' do + get '/business' + + assert_equal [I18n.t('site.partners.manufacturers'), I18n.t('site.partners.integrators')], + css_select('h3.text-uppercase').map { |h| h.text.strip } + end + + # The dark band arriving with no gap under an article on white reads as the + # text falling off a cliff. The homepage is the exception: its band follows a + # tinted full-bleed section, where the gap is a white stripe between two + # coloured bands. + test 'the closing band keeps its distance from text on white' do + %w[/get-started /low-latency /community /ecosystem].each do |path| + get path + + band = css_select('section.section--ink').last + + assert_not_nil band, "#{path} has no closing band" + assert_includes band['class'], 'mt-6', "#{path} runs its text straight into the band" + end + + get '/' + assert_not_includes css_select('section.section--ink').last['class'], 'mt-6', + 'the homepage band is separated from the section above it by a white stripe' + end + + # Cryptocurrency is off the donation page; Open Collective stays. + test '/donate offers Open Collective and nothing crypto' do + get '/donate' + + assert_includes response.body, 'opencollective.com/openipc' + assert_not_includes response.body, 'bi-currency-bitcoin' + assert_not_includes response.body, 't.me/wallet' + %w[en ru zh].each do |locale| + assert_nil I18n.t('pages.donate.crypto_title', locale: locale, default: nil), + "the crypto copy is still defined in #{locale}" + end + end + + # :exhibitions is held in the system on purpose and shown on no page. There + # is no trade-show page yet; when there is, it asks for the group and the + # logo and link are already there. Until then this is what keeps it off the + # site -- a group is easy to add to a row by accident. + test 'the exhibitions group is kept but rendered nowhere' do + held = PagesHelper::PARTNER_GROUPS.fetch(:exhibitions) + + assert_not_empty held + assert_not_includes PagesHelper::HOME_PARTNER_ROWS.values.flatten, :exhibitions + + %w[/ /business /ecosystem /community /donate /low-latency /get-started].each do |path| + get path + + held.each do |logo| + assert_not_includes response.body, logo[:img].sub('.png', ''), + "#{logo[:name]} is being rendered on #{path}" + end + end + end + # The radio-link card names devourer, OpenIPC's own userspace Realtek driver, # rather than wfb-ng. wfb-ng keeps its place in the credits -- the link exists # because of both -- so the test checks the card, not the whole page. @@ -165,7 +311,7 @@ class RelaunchPagesTest < ActionDispatch::IntegrationTest assert_not_nil card, 'no card on the page names devourer' assert_not_empty card.css('a[href="https://github.com/OpenIPC/devourer"]'), 'devourer is named but not linked' - assert_includes response.body, I18n.t('pages.low_latency.credits_text'), + assert_includes response.body, I18n.t('pages.low_latency.credits_text_html'), 'the credits no longer thank both projects' end