惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Hacker News
The Hacker News
Martin Fowler
Martin Fowler
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
U
Unit 42
F
Full Disclosure
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Recorded Future
Recorded Future
Security Archives - TechRepublic
Security Archives - TechRepublic
阮一峰的网络日志
阮一峰的网络日志
T
Threatpost
P
Privacy International News Feed
GbyAI
GbyAI
Stack Overflow Blog
Stack Overflow Blog
MongoDB | Blog
MongoDB | Blog
I
Intezer
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Privacy & Cybersecurity Law Blog
A
Arctic Wolf
博客园 - 聂微东
博客园 - 叶小钗
Cisco Talos Blog
Cisco Talos Blog
H
Help Net Security
S
Schneier on Security
Y
Y Combinator Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
The Exploit Database - CXSecurity.com
T
Tor Project blog
月光博客
月光博客
NISL@THU
NISL@THU
A
About on SuperTechFans
Spread Privacy
Spread Privacy
Blog — PlanetScale
Blog — PlanetScale
D
DataBreaches.Net
雷峰网
雷峰网
C
CXSECURITY Database RSS Feed - CXSecurity.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 【当耐特】
G
Google Developers Blog
W
WeLiveSecurity
P
Palo Alto Networks Blog
The Last Watchdog
The Last Watchdog
K
Kaspersky official blog
博客园 - 司徒正美
L
LINUX DO - 热门话题
小众软件
小众软件

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
The 3 i18n mistakes every open-source LMS makes
Vadym Arnaut · 2026-05-13 · via DEV Community

TL;DR. Every open-source LMS treats internationalization as one problem. It's three. UI strings, user-generated content, and canonical artifacts each need a different mechanism. Most codebases collapse them into one — that's the bug.

We've been building an open-source Bible school LMS for about 5 months. It runs in Russian and English (Ukrainian coming), with auto-translated user content and canonical-text preservation for scripture quotes. Building this forced me to look at how Moodle, Open edX, Canvas LMS, and Chamilo handle the same problem.

The pattern is the same in all of them — and was the same in ours when we started.


Mistake #1: User-generated content treated like UI strings

Every LMS has solid gettext-style infrastructure for UI strings. "Sign in", "Course catalog", "Submit assignment" — these live in .po files (Moodle), Transifex (Open edX), i18n-js (Canvas), YAML (Rails-based). A translator translates the file once. Done.

User-generated content is a different problem entirely. When a teacher authors a course in Russian, the title — "Введение в Послание к Римлянам" — is a row in courses.title. An English-speaking student opens the catalog and sees Cyrillic. The UI is translated. The content isn't.

UI strings User-generated content
Examples "Sign in", "Submit" Course title, lesson body, quiz question
Source Fixed catalog of values Unbounded user input
Tool gettext / Transifex / YAML Runtime translation (Google, DeepL, Gemini)
When Build / release time Runtime (lazy or eager)
Storage Locale file Separate cache table
Common bug None major Treated like UI strings → only one language ever shown

Moodle's workaround is the multilang filter — you wrap content in <span lang="ru">…</span><span lang="en">…</span> and a filter shows the matching one. This is a 2008 solution. It puts the entire burden on the teacher: they must author every piece of content twice, in every language. Most don't, and the platform falls back to "show whatever the teacher wrote first."

The shape of the right answer is a separate translation cache:

CREATE TABLE content_translations (
  entity_type  TEXT NOT NULL,    -- 'course', 'lesson', 'quiz_question'
  entity_id    UUID NOT NULL,
  field        TEXT NOT NULL,    -- 'title', 'description', 'body'
  locale       TEXT NOT NULL,    -- 'ru', 'en', 'uk', 'es'
  content      TEXT NOT NULL,
  source       TEXT NOT NULL,    -- 'human' | 'machine' | 'canonical'
  cached_at    TIMESTAMPTZ NOT NULL,
  PRIMARY KEY (entity_type, entity_id, field, locale)
);

Enter fullscreen mode Exit fullscreen mode

Teacher authors in one language. A translation worker fills in the others lazily (first request) or eagerly (on publish). The course-detail endpoint joins on (entity_type, entity_id, field, viewer_locale).

The architectural decision: content is not a UI string and cannot live in the same system. If your LMS uses gettext for "Submit assignment" and the same mechanism for course titles, that's the bug.


Mistake #2: No accommodation for length variance

English is dense. Russian runs ~25–30% longer for the same meaning. German is comparable. Finnish is worse. Arabic is shorter — and right-to-left, its own category.

Most LMS UIs are designed in English. Buttons that fit "Save" don't fit "Сохранить". Nav tabs that fit "Courses" wrap to two lines for "Курсы и обучение". Mobile breaks first.

Part of the fix is CSS:

/* Reserve space against the longer-language baseline */
.action-button {
  min-width: 8rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Tabular content: don't let translation reflow the grid */
.lesson-list-cell {
  min-height: 4.5rem;  /* fits 2-line Russian titles without shift */
}

Enter fullscreen mode Exit fullscreen mode

But the real fix is testing every screen in your longest-content language, not just English. Most teams hire designers who only see the English mock and don't realize their "Continue" button is broken in Russian until a user reports it.

What helped us: a Storybook locale switcher defaulting to Russian (not English), and a Playwright snapshot suite that screenshots both locales. The first commit that breaks Russian layout is caught in CI, not by a user.


Mistake #3: Canonical content forced through translation

This is the bug that motivated our entire content-translation rewrite.

A teacher in Russian writes: "Послание к Римлянам 8:28 говорит, что все содействует ко благу". The course gets auto-translated to English. Naively you send the whole string to Google Translate or DeepL, and you get back: "Romans 8:28 says that everything works together for good." That's almost a real Bible verse — but it isn't quoting any actual translation. It's a translation of a translation.

You don't want that. You want the actual KJV (or NIV, or ESV) text of Romans 8:28 spliced in.

Same problem exists everywhere there's canonical content:

  • Programming courses with code samples (don't translate the variables!)
  • Math curricula with formulas
  • Legal courses citing statutes
  • Medical courses citing clinical-trial registries
  • Literature courses with original-language quotes

Most LMSes don't separate "translatable prose" from "canonical artifact" — so when they auto-translate, the canonical content gets mangled or invented.

Our pattern: parse the source for canonical references, replace each with a placeholder token, translate the surrounding prose, then substitute the canonical text back from a separate lookup.

def translate_with_canonical_preservation(
    text: str, source_lang: str, target_lang: str
) -> str:
    # 1. Find canonical references in either language
    refs = extract_bible_refs(text, lang=source_lang)
    # [{"raw": "Послание к Римлянам 8:28", "book": "ROM", "chapter": 8, "verses": [28]}]

    # 2. Replace each with a unique placeholder
    placeholders = {}
    for i, ref in enumerate(refs):
        token = f"⟦CANON_{i}"
        placeholders[token] = ref
        text = text.replace(ref["raw"], token, 1)

    # 3. Translate the token-bearing string
    translated = translate(text, source_lang, target_lang)

    # 4. Substitute the canonical text back, looked up in the target translation
    for token, ref in placeholders.items():
        canonical_text = lookup_canonical(ref, target_lang)  # KJV for en, Synodal for ru
        translated = translated.replace(token, canonical_text)

    return translated

Enter fullscreen mode Exit fullscreen mode

How extract_bible_refs handles the cross-language book-name matrix

Detection is a regex over a normalized book-name dictionary. Each canonical book has an entry like:

BOOKS = {
    "ROM": {
        "en": ["Romans", "Rom", "Rom."],
        "ru": ["Послание к Римлянам", "Римлянам", "Рим", "Рим."],
        "uk": ["Послання до Римлян", "Римлян", "Рим"],
    },
    # ... 65 more books
}

Enter fullscreen mode Exit fullscreen mode

The regex is built per request as (book_alias_1|book_alias_2|...)\s+(\d+):(\d+)(?:[-–](\d+))? so it accepts: Romans 8:28, Послание к Римлянам 8:28, Рим 8:28, Рим. 8:28, Romans 8:28-29, Romans 8:28a.

Sundry edge cases: chapter-only refs (Romans 8 — whole-chapter), letter suffixes (8:28a — first half of verse), em-dash vs hyphen, non-breaking spaces. Each lives in unit tests.

lookup_canonical pulls from a canonical text table keyed by (book, chapter, verse_start, verse_end, translation). Cache the final translated string keyed by (entity_id, field, locale) per Mistake #1.

This is ~400 lines we wouldn't have written if any of the LMSes we looked at had solved this for us.


What I want to hear back

These are the three patterns I keep seeing. They're not the only ones (cache invalidation on edits, RTL layout, Slavic plural forms — separate posts), but they're the ones every general-purpose LMS skips.

If you've shipped i18n in an LMS, education platform, or any content product:

  • Do you separate UI strings from content? What's your storage shape?
  • How do you handle length variance? Do you test the long-language layout in CI?
  • Do you have canonical content that mustn't be translated? Code samples, equations, citations? How are you handling it?

Curious where teams have landed. The patterns above are our current best, not our last word.


The project that drove all of this is open source:

GitHub logo ArVaViT / equip

Free, open-source LMS for Bible schools, ministries, and nonprofit educational programs. React + FastAPI + Supabase.

Equip logo

Equip

A free, open-source learning management system built for Bible schools church ministries, and nonprofit educational programs

MIT License Backend CI Frontend CI Good first issues

Live demo · Roadmap · Contributing · Changelog


Why this project?

Hundreds of small Bible schools, home churches, and missionary training programs around the world still manage courses on paper, WhatsApp, or spreadsheets. Commercial LMS platforms are expensive, overkill, or require technical expertise that volunteer-run organizations simply don't have.

Equip is designed to change that:

  • Free forever — MIT-licensed, no paywalls, no "premium" tiers.
  • Simple to deploy — one-click Vercel deploy with a free Supabase database. No Docker, no servers to manage.
  • Built for small scale — optimized for 20-100 students, not enterprise pricing models.
  • Contributor-friendly — clear docs, conventional commits, issue templates, and a welcoming community.

Features

Area What you get
Course authoring Courses, modules, chapters, rich content blocks (TipTap editor with images, YouTube, callouts, audio)
Assessments Multiple-choice, true/false, short-answer, and essay