DEV Community

Northbeam Technologies
Northbeam Technologies

Posted on

Building an RTL Hebrew Video Platform with Nuxt 2 SSR and Video.js

The Problem

Hebrew is a right-to-left language. Building a video platform where Hebrew is the primary language — not an afterthought — means RTL has to be the default at every layer: layout, navigation, text direction, input alignment, and icon mirroring. On top of that, the site needed server-side rendering for SEO (video titles, channel names, and category pages all need to be crawlable), and a player that remembers exactly where a user left off across sessions and devices.

What We Built

kolel.org video page — RTL Hebrew layout, Video.js player, sidebar playlist, and watch/share actions.

kolel.org is a Nuxt 2 universal app — SSR on first load, client-side navigation thereafter. Vuetify 2 handles the UI with RTL enabled globally. @nuxtjs/i18n manages locale detection with Hebrew (he) as the default and English (en) as a fallback. Video playback uses Video.js 7 with a custom Vue wrapper that tracks position and syncs it to the Rails API on a 5-second interval.

How It Works

Request flow: Nuxt SSR renders Hebrew RTL HTML → Video.js hydrates on client → watch position syncs to Rails API.

Key Technical Decisions

1. Vuetify RTL enabled globally

Setting rtl: true in vuetify.options.js flips all Vuetify components — navigation drawers open from the right, icons mirror, flex directions invert, and text aligns right by default. One flag, entire UI:

// vuetify.options.js
export default {
  defaultAssets: {
    font: false,
    icons: 'mdi'
  },
  theme: {
    themes: {
      light: { primary: '#1e88e5' },
      dark:  { primary: '#1e88e5' }
    }
  },
  rtl: true  // flips the entire Vuetify UI to right-to-left
}
Enter fullscreen mode Exit fullscreen mode

2. Hebrew as the default locale, no URL prefix

@nuxtjs/i18n is configured with strategy: 'no_prefix' so Hebrew URLs are clean (e.g. /video/123 not /he/video/123). Browser language detection is disabled — every visitor gets Hebrew first, with English available via a manual toggle:

// config/i18n.js
const messages = {
  en: { ...require('../locales/en.json'), $vuetify: en, dir: 'ltr' },
  he: { ...require('../locales/he.json'), $vuetify: he, dir: 'rtl' },
}

export default {
  locale: 'he',
  fallbackLocale: 'en',
  messages
}

// nuxt.config.js
i18n: {
  locales: ['en', 'he'],
  defaultLocale: 'he',
  localeDetection: false,
  detectBrowserLanguage: false,
  strategy: 'no_prefix',
  vueI18n: i18n
}
Enter fullscreen mode Exit fullscreen mode

3. Video.js player with watch-position tracking

The player is a Vue component that mounts Video.js imperatively in mounted(). A setInterval running every 5 seconds calls storeVideoPosition(), which sends the current playback time to the Rails API — so a user who closes the tab resumes from the exact second on any device:

import videojs from 'video.js';

// mounted()
this.player = videojs(this.$refs.videoPlayer, this.playerOptions);

this.player.ready(() => {
  this.playVideo(this.videoUrl);
});

this.player.on('ratechange', this.playerRateChanged);
this.player.on('play',  this.playing);
this.player.on('pause', this.stopInterval);
this.player.on('ended', this.stopInterval);

// watch-position sync — fires every 5 s while playing
storeVideoPosition() {
  const payload = {
    id: this.video.id,
    position: Math.ceil(this.player.currentTime()),
  };
  this.video.user_watch = {
    ...(this.video.user_watch || {}),
    position: payload.position,
  };
  this.markWatch(payload); // Vuex action → Rails API PATCH /watches/:id
}
Enter fullscreen mode Exit fullscreen mode

4. SSR + hydration without layout shift

Because Video.js requires a real DOM, the player component is wrapped in a <client-only> tag in Nuxt. The server renders the video metadata, title, and channel info (crawlable by search engines) while the player hydrates client-side after JavaScript loads — no layout shift because the player slot is sized with CSS before hydration.

Results / What Changed

  • Full Hebrew RTL layout across every page — no per-component dir overrides needed
  • Video titles, channel pages, and category routes are indexed by search engines via SSR
  • Watch position resumes cross-device — creators report viewers completing more long-form lessons
  • English toggle works without a page reload — Vuetify + i18n switch both layout direction and copy simultaneously

Stack

Nuxt 2.15 · Vue 2 · Vuetify 2 (RTL) · @nuxtjs/i18n · Video.js 7 · Axios · Rails 7 API · Google Cloud Storage


Building an internationalized or RTL web platform? Get in touch.

Originally published on the Northbeam Technologies Insight.

Top comments (2)

Collapse
 
pakvothe profile image
Franco Ortiz

Good writeup, the "RTL has to be the default at every layer" point is something you only learn by shipping one of these.

Since you do this across client projects: the layout layer you have to solve per-project, but the translation pipeline you can standardize. I built i1n (i1n.ai) for that part, locale JSONs as source of truth, AI translation with context, typed keys, CI check for drift, works with @nuxtjs/i18n files. For an agency it means the i18n setup cost drops on every project after the first. Free tier per project to try it.

Collapse
 
northbeamtech profile image
Northbeam Technologies

Thanks! Yeah, RTL bugs have a way of hiding until you actually ship to real users. The i18n pipeline point is fair — will check out i1n, appreciate the tip.