开发者
Use Google Sheets as a Translation Database for Your Web App (Apps Script + Next.js)
Every i18n setup I've seen has the same three-way standoff. Developers want type-safe JSON in the repo. Translators want a familiar tool, not a pull request. Product wants to fix a typo without a deploy. So you either pay $50–$500/month for a localization SaaS, or you copy-paste strings between a translator's spreadsheet and your JSON files until something silently breaks. For projects under ~1,000 keys, there's a better middle: the spreadsheet is the database. Translators edit a Google Sheet; an Apps Script endpoint serves it as clean locale JSON; your app pulls that at build time. Here's the whole pattern, with the code. Why a sheet beats a translation service for small projects A localization SaaS earns its price at scale — dozens of translators, thousands of keys, screenshots and review workflows. A 300-key marketing site doesn't have that problem; it has a coordination problem. A Sheet solves coordination for free: translators already know it, it has revision history and suggested edits built in, and product can change a string in ten seconds. You only add the two things a raw sheet lacks — a clean JSON API and a fallback for missing translations. The schema: one tab, one row per key A strings tab, with the key in column A and one column per locale: key en tr es fr hero.title Welcome Hoş geldiniz Bienvenido Bienvenue hero.cta Get started Başla Empezar Commencer Use dot-notation keys ( hero.title ) so the JSON nests naturally in your i18n library. Keep a tiny meta tab too: B1 = default locale ( en ), B3 = version ( 1.0.0 ). The Apps Script endpoint Deploy this as a Web App (same mechanics as any Apps Script webhook ). doGet serves one locale — or all of them — as JSON, and the fallback lives right in the query: an empty cell resolves to the default locale, so a half-translated key never ships blank. // Code.gs const SHEET_ID = ' your-sheet-id ' ; function doGet ( e ) { const locale = ( e . parameter . locale || ' all ' ). toLowerCase (); const result = buildLoca
AI 资讯
MOKSHA Devlog: Why My Game Worked on Itch.io but Died on GitHub Clone (The .gitignore Trap) 🤡
Hey DEV Community! 👋 I am currently building MOKSHA, an HTML5 Canvas game deeply rooted in Vedic philosophy. The game involves managing your Karma, avoiding Maya (Illusions), and achieving spiritual liberation. Ironically, while building a game about waking up from cosmic illusions, I fell into a technical illusion myself yesterday. Let me tell you a chaotic detective story about how my game froze on a fresh repository clone, and how I found the silent assassin hiding in plain sight. 🤡 🚫 The Disaster: Works on Itch.io, Freezes on GitHub So, there I was, ready to release a fresh update. I generated my build packages locally, zipped them up, and proudly uploaded them to Itch.io. I hit Publish, tested the live link, and everything worked flawlessly. High scores, smooth frames, total spiritual awakening. Then, I casually walked over to my terminal, ran git add . followed by git push, and went to bed thinking I was an absolute pro. The next morning, I wanted to double-check my clean repository, so I cloned it fresh into a new folder. I booted up the local server, and... the entire game was completely unclickable. Dead clicks. Frozen canvas. Total illusion (Maya). 💀 Opening up the browser console revealed a fierce wall of red text: style.min.css:1 Failed to load resource: the server responded with a status of 404 (Not Found) main.min.js:1 Failed to load resource: the server responded with a status of 404 (Not Found) 🕵️♂️ The Realization: It Wasn't Me, It Was My .gitignore! Initially, I blamed my sleep-deprived brain, thinking I forgot the chronological order of pushing and building. But when I opened my root directory to inspect the crime scene, I found the real culprit staring right back at me on lines 46 and 50 of my .gitignore file: dist/ *.zip index.min.html The Ultimate Trap Exposed 🪤 Because dist/ was explicitly blacklisted in my .gitignore, Git was literally doing its job perfectly by completely ignoring my production builds during staging! Here is exactly how the
AI 资讯
The 300px Canvas Bug That Shrunk My React Image Editor
This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry . Project Overview I am building a browser-based text removal workspace where a user uploads an image, paints over unwanted text or objects, and sends the resulting mask to an image-editing pipeline. The mask editor uses three stacked <canvas> elements: a base canvas for the uploaded image; an overlay canvas for the painted mask; a cursor canvas for the brush preview and pointer events. All three canvases must have identical dimensions. The pointer coordinates must also map back to the same bitmap coordinate system, or the generated mask will not match the part of the image the user selected. Bug Fix On desktop, the editor had plenty of horizontal space but the uploaded image appeared inside a narrow strip surrounded by a large empty area. The result preview used the available width correctly, so the two sides of the same workspace looked unrelated. The visible symptom was a tiny image editor. The actual failure started before the image was drawn. The initialization code measured the width of the canvas wrapper: const container = canvas . parentElement if ( ! container ) return const containerWidth = container . clientWidth || 1 const containerHeight = 600 It then calculated the largest canvas size that would preserve the uploaded image's aspect ratio: const imgAspectRatio = img . width / img . height const containerAspectRatio = containerWidth / containerHeight let canvasWidth : number let canvasHeight : number if ( imgAspectRatio > containerAspectRatio ) { canvasWidth = containerWidth canvasHeight = containerWidth / imgAspectRatio } else { canvasHeight = containerHeight canvasWidth = containerHeight * imgAspectRatio } The aspect-ratio calculation was correct. The measurement it received was not. Root Cause: The Canvas Measured Itself The wrapper was a relatively positioned element with no declared width: < div className = "relative transition-all duration-500 ease-out" style = { {
AI 资讯
Rino.js 3, Building Modern Websites Without a Frontend Framework
Modern web development has become incredibly powerful. But also increasingly complicated. Many projects begin by installing hundreds of megabytes of dependencies before writing a single page. Frameworks, bundlers, routers, templating systems, CSS tooling, and runtime libraries all solve important problems, but they also introduce additional complexity. I wanted something different. I wanted to build websites that start with plain HTML, while still providing the features developers expect today: Reusable components Markdown support TypeScript CSS and JavaScript bundling Internationalization (i18n) Content collections RSS/Atom feeds Sitemap generation Fast development builds That idea became Rino.js. What is Rino.js? Rino.js is an HTML-first website compiler for building static websites, documentation, blogs, portfolios, company websites, and other content driven projects. Instead of introducing a custom templating language or requiring a frontend framework, Rino.js treats HTML as the primary language. Pages remain valid HTML while additional functionality is added through a small set of build-time conventions. The goal is simple: Write HTML. Generate optimized static websites. Why HTML First? HTML has existed for decades, yet modern web development often treats it as something generated by another language. Rino.js takes the opposite approach. Instead of writing components in JSX or another template language, components are simply HTML files. <component rino-import= "header" ></component> That's all it takes. The compiler replaces the component during the build, producing plain static HTML with no runtime dependency. Starting Rino.js Rino.js has a command that is designed to provide default project. npm create rino@latest Project Shape A Rino.js project usually looks like this: my-site/ rino-config.js dev.js generate.js feed.js sitemap.js backoffice.js pages/ index.html about.html components/ header.html footer.html public/ images/ photo.webp scripts/ export/ app.js
AI 资讯
Getting Started with Ant Design — Build Your First React UI in 15 Minutes
What Is Ant Design? Ant Design (antd) is a React UI library built by Alibaba's Ant Group. It's the most starred React component library on GitHub from China, with over 90k stars — yet surprisingly undercovered in the English-speaking developer community. If you've used Material UI or Chakra UI, Ant Design is the Chinese equivalent, but with its own design philosophy: consistent, predictable, and packed with enterprise-grade components out of the box. Fun fact: Alibaba, Tencent, Baidu, and most Chinese tech companies use Ant Design in production. It powers dashboards that serve hundreds of millions of users. Why Ant Design Over MUI? Feature Ant Design Material UI Components 60+ 50+ Table (Pro) Built-in sorting, filtering, pagination, row selection Requires manual wiring Form validation Declarative, built-in Requires react-hook-form or Formik Tree-shaking Supported (v5) Supported Bundle size (min) ~200KB gzipped ~140KB gzipped Documentation Chinese-first, English translations available English-first Design system Ant Design System (custom) Material Design (Google) Ant Design wins on out-of-the-box productivity — especially for data-heavy apps like admin panels and dashboards. MUI wins on bundle size and first-party English docs. Installation npm install antd @ant-design/icons No peer dependencies beyond React 16+. Your First Ant Design Component import React from " react " ; import { Button , Space } from " antd " ; import { SearchOutlined , DownloadOutlined } from " @ant-design/icons " ; export default function App () { return ( < Space > < Button type = "primary" icon = { < SearchOutlined /> } > Search </ Button > < Button icon = { < DownloadOutlined /> } > Download </ Button > < Button type = "dashed" > Dashed </ Button > < Button type = "link" > Link </ Button > </ Space > ); } That's it. Five button variants with zero CSS. Building a Data Table in 5 Minutes import React , { useState , useMemo } from " react " ; import { Table , Input } from " antd " ; const data
AI 资讯
What Actually Happens After You Send a Webhook
The first version of a webhook is always the same four lines: await fetch(customer.webhookUrl, { ...
开发者
It Was Just a Patch Update. What Could Possibly Go Wrong?
This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry. You know those...
AI 资讯
My Internship Journey: Learning Beyond the Classroom
Internships are one of the most valuable experiences for any undergraduate, and I am grateful to have completed mine. This journey allowed me to bridge the gap between academic knowledge and real-world software development while improving both my technical and professional skills. From my very first day, I was introduced to a collaborative development environment where teamwork, communication, and problem-solving played a major role. I had the opportunity to work on real projects, understand industry workflows, and learn how professional software products are built and maintained. Throughout my internship, I gained hands-on experience with modern web technologies, version control using Git, API integration, debugging, and deploying applications. I also learned the importance of writing clean, maintainable code and following industry best practices. Working alongside experienced developers helped me improve my coding standards and exposed me to new tools and frameworks. One of the biggest lessons I learned was that software development is not only about writing code. It involves understanding user requirements, collaborating with team members, managing deadlines, and continuously learning new technologies. Every challenge I encountered became an opportunity to grow and improve my skills. Beyond technical knowledge, this internship strengthened my confidence, communication, time management, and ability to work effectively in a professional team. The guidance and support from my mentors played a significant role in my growth throughout this journey. Looking back, this internship has been a milestone in my career. It has given me practical experience, valuable industry exposure, and a clearer vision of the software engineering field. I am excited to apply these lessons in my future projects and continue growing as a developer. I would like to express my sincere gratitude to my mentors, teammates, and the organization for providing me with this incredible opportunity. Th
开发者
Join our latest Frontend Challenge: Comfort Food Edition 🍲
We're back with another Frontend Challenge, and this time we're hungry! 🍜🥧 Running through August 16 , Frontend Challenge: Comfort Food Edition invites you to build something inspired by the food that makes you feel at home. Show off the dish you make when nothing else will do, build a site for a restaurant that exists (or one that only lives in your head), share the recipe you've been perfecting for years, or put a spotlight on a regional dish that deserves more attention. Whether you're a CSS connoisseur, a JavaScript chef, or somewhere in between, there's a prompt here for you. We hope you give it a try! The Prompts CSS Art: Comfort Food Create a work of art using primarily CSS! Let food be your inspiration: a steaming bowl of ramen, a stack of pancakes, a perfectly cut slice of pie, or the dish you grew up eating. CSS Art Submission Template Note: We're now allowing a sprinkle of JavaScript in CSS Art submissions! However, judging will continue to focus primarily on the CSS component, so keep JavaScript usage light and purposeful. The star of the show should still be your CSS skills. Perfect Landing: Comfort Food Build a polished, functional landing page with a food theme. This could be a real or imaginary restaurant, a recipe collection, a food festival, a love letter to a regional dish, or anything else you can imagine, as long as it captures the theme and demonstrates excellent frontend fundamentals. Perfect Landing Submission Template Note: You may use JavaScript, TypeScript, Dart, WebAssembly, or any other browser-compatible language/runtime in your Perfect Landing submissions! Show us what modern web development can do. Judging Criteria and Prizes CSS Art submissions will be evaluated on: Creativity Effective Use of CSS Aesthetic Outcome Perfect Landing submissions will be evaluated on: Accessibility Usability and User Experience Creativity Code quality Prizes Each prompt winner will receive a DEV++ Membership and an exclusive DEV Badge. All Participants w
AI 资讯
Unknown Time Is Not Noon: Modeling Missing Temporal Data Without Inventing Facts
Missing data is not the same thing as a convenient default. That sounds obvious, yet temporal software regularly converts an empty time field into midnight, noon, the current time, or the start of a day. The interface may look complete after that conversion, but the program has silently changed an unknown fact into a known one. This matters anywhere an hour can change the result: medical timelines, transport schedules, legal deadlines, astronomical calculations, historical records, and calendrical systems. I encountered the problem while working with a BaZi calculation pipeline. A BaZi chart can use year, month, day, and hour components. If the birth time is absent, the honest result is a three-component analysis with hour-dependent conclusions withheld. Inserting noon would make the output look richer while making its provenance weaker. The useful engineering question is not “Which fallback time should we choose?” It is “How do we keep uncertainty visible through every layer of the system?” The public calculation evidence repository provides the concrete calendar-domain fixtures referenced below. The rest of this article focuses on the reusable software boundary behind them. Model knowledge, not just a string A common input model makes absence too easy to erase: const birthTime = form . time || " 12:00 " ; After this line runs, downstream code cannot tell whether noon came from the user or the fallback. Validation, analytics, caching, and the result renderer all see the same string. The information loss happens before the calculation begins. A small discriminated union keeps the two states separate: /** * @typedef {{ kind: "known", localTime: string, source: "user" }} * KnownTime * @typedef {{ kind: "unknown" }} UnknownTime * @typedef {KnownTime | UnknownTime} BirthTime */ function parseBirthTime ( value ) { const normalized = value ?. trim (); return normalized ? { kind : " known " , localTime : normalized , source : " user " } : { kind : " unknown " }; } This typ
开源项目
🔥 visualbruno / 3DGenStudio
GitHub热门项目 | | Stars: 414 | 39 stars today | 语言: JavaScript
开源项目
🔥 shaka-project / shaka-player - JavaScript player library / DASH & HLS client / MSE-EME play
GitHub热门项目 | JavaScript player library / DASH & HLS client / MSE-EME player | Stars: 8,174 | 3 stars today | 语言: JavaScript
开源项目
🔥 fspecii / ace-step-ui - 🎵 The Ultimate Open Source Suno Alternative - Professional U
GitHub热门项目 | 🎵 The Ultimate Open Source Suno Alternative - Professional UI for ACE-Step 1.5 AI Music Generation. Free, local, unlimited. Stop paying for Suno! | Stars: 4,596 | 82 stars today | 语言: JavaScript
AI 资讯
Compressing an image to exactly 50KB in the browser, with no server
Indian government exam portals have a rule that has quietly shaped a lot of my code: your photo must be under 50KB . Not "small". Not "optimised". Under 50KB, or the upload is rejected. Every free tool I found for this wanted me to upload the photo to a server, wait in a queue, and create an account. For a file I just wanted to shrink. So I wrote it myself, in the browser. This post is about the actual technique — hitting an exact byte target with canvas — and, honestly, about where my implementation still falls short. The naive version, and why it fails The obvious approach: canvas . toBlob ( blob => download ( blob ), ' image/jpeg ' , 0.7 ); Pick a quality, hope for the best. The problem is that JPEG quality has no predictable relationship to output size. Quality 0.7 on a flat, low-detail portrait might land at 18KB. The same 0.7 on a noisy, high-detail photo lands at 210KB. You cannot compute the quality you need — the encoder decides, and it depends entirely on image content. So you can't calculate it. You have to search for it. Binary search on quality toBlob is cheap enough to call repeatedly, and quality is monotonic — higher quality never produces a smaller file. That's exactly the setup binary search wants. function encode ( canvas , quality ) { return new Promise ( resolve => canvas . toBlob ( resolve , ' image/jpeg ' , quality ) ); } async function compressToTarget ( canvas , targetBytes ) { let lo = 0.05 , hi = 0.95 , best = null ; for ( let i = 0 ; i < 8 ; i ++ ) { const mid = ( lo + hi ) / 2 ; const blob = await encode ( canvas , mid ); if ( blob . size <= targetBytes ) { best = blob ; // fits — remember it, try for better quality lo = mid ; } else { hi = mid ; // too big — back off } } return best ; } Eight iterations over the range 0.05–0.95 narrows quality to about ±0.002, far finer than anyone can see. Each iteration is one encode; on a typical phone photo the whole loop runs in well under a second. Two details that matter more than they look: Keep
AI 资讯
Handling Asynchronous Webhook Notifications & Callbacks in Joget via BeanShell
Handling Asynchronous Webhook Notifications & Callbacks in Joget via BeanShell When integrating Joget DX with external platforms—such as payment gateways, SMS providers, or ERP systems—requests are often processed asynchronously. The external system accepts a request immediately and dispatches an HTTP POST webhook callback to Joget minutes or hours later when processing completes. Receiving webhook callbacks inside BeanShell API endpoints requires two key tasks: Safe Variable Type Coercion: Handling parameter arrays ( String[] ) versus single strings ( String ) safely without throwing ClassCastException . FormDataDao Persistence: Saving or updating the notification payload inside a Joget form database table using FormDataDao . In this guide, we'll write a defensive Java/BeanShell script that receives asynchronous webhook callbacks and logs them cleanly into Joget. Architecture Overview Webhook Endpoint: An external system hits your Joget API endpoint with callback parameters (e.g. process_id , status , response_payload , recipient ). Type Extraction: A safe helper function handles parameter type variations (whether passed via URL query params or JSON request bodies). FormDataDao Save: Instead of executing raw JDBC queries, the script uses FormDataDao to persist a FormRowSet directly into Joget's form storage engine. The BeanShell Script Place this code inside your API Builder BeanShell script or custom REST endpoint: import org.joget.apps.app.service.AppUtil ; import org.joget.apps.form.dao.FormDataDao ; import org.joget.apps.form.model.FormRow ; import org.joget.apps.form.model.FormRowSet ; import org.joget.commons.util.LogUtil ; import java.util.UUID ; // 1. Safe Type Extraction Helper public String safeExtract ( Object param ) { if ( param == null ) return "" ; try { if ( param instanceof String []) { String [] arr = ( String []) param ; return arr . length > 0 ? arr [ 0 ] : "" ; } if ( param instanceof String ) { return ( String ) param ; } } catch ( Throwable t
AI 资讯
Generating Multilingual HTML Reports with Attachment Download Links in Joget
Generating Multilingual HTML Reports with Attachment Download Links in Joget Creating customized executive report summaries in Joget DX often requires more than simple database lists. Real-world business reports frequently need to join multiple tables, translate status labels based on the user's active locale ( #platform.currentLocale# ), and generate secure file download links for form attachments. In this guide, we'll build a Java/BeanShell script that queries main records and history logs, resolves internationalization ( i18n ) message keys dynamically, and generates interactive HTML reports embedded with secure attachment links. Key Components Dynamic i18n Translation: Uses AppUtil.processHashVariable("#i18n.key#", null, null, null) to convert database status codes into localized text matching the user's language setting. File Attachment Links: Formats secure file download URLs ( /jw/web/client/app/{appId}/{version}/form/download/{tableName}/{recordId}/{fileName} ) so users can open uploaded documents directly from the report summary. Multi-Table SQL Join: Merges main request details, audit transaction history, and custom review tables into a clean HTML document layout. The BeanShell Script Place this code inside a BeanShell Form Bounding Box or an HTML Report Generator tool step: import java.sql.Connection ; import java.sql.PreparedStatement ; import java.sql.ResultSet ; import java.net.URLEncoder ; import javax.sql.DataSource ; import org.joget.apps.app.service.AppUtil ; import org.joget.apps.app.model.AppDefinition ; import org.joget.commons.util.LogUtil ; // Helper: Resolve i18n hash variables dynamically public String getLocalizedText ( String messageKey ) { if ( messageKey == null || messageKey . isEmpty ()) return "" ; String hashVariable = "#i18n." + messageKey + "#" ; return AppUtil . processHashVariable ( hashVariable , null , null , null ); } String recordId = "#requestParam.id#" ; if ( recordId == null || recordId . trim (). isEmpty ()) { return "<di
AI 资讯
How to Update Joget App Environment Variables Programmatically in BeanShell
How to Update Joget App Environment Variables Programmatically in BeanShell In Joget DX, App Environment Variables are commonly used to store global configuration values—such as API endpoints, tax rates, batch counter sequences, or feature flags. While administrators can update these variables manually through Joget App Center, enterprise workflows often need to update environment variables programmatically (for example, incrementing a daily batch sequence counter or updating an OAuth access token). In this guide, we'll write a short BeanShell script using Joget's EnvironmentVariableDao to fetch and update App Environment Variables dynamically. How It Works Obtain App Context: AppUtil.getCurrentAppDefinition() retrieves the active application definition. Access the DAO Bean: AppUtil.getApplicationContext().getBean("environmentVariableDao") retrieves Joget's internal DAO for environment variables. Load & Update: environmentVariableDao.loadById(envVarId, appDef) retrieves the target variable instance. Modifying .setValue() and executing environmentVariableDao.update(envVar) persists the updated value immediately. The Code Place this BeanShell snippet inside a BeanShell Tool workflow step or a Form Post-Processing Tool : import org.joget.apps.app.dao.EnvironmentVariableDao ; import org.joget.apps.app.model.AppDefinition ; import org.joget.apps.app.model.EnvironmentVariable ; import org.joget.apps.app.service.AppUtil ; import org.joget.commons.util.LogUtil ; public void updateAppEnvironmentVariable ( String variableId , String newValue ) { AppDefinition appDef = AppUtil . getCurrentAppDefinition (); if ( appDef != null ) { // Retrieve Joget's Environment Variable DAO bean EnvironmentVariableDao envDao = ( EnvironmentVariableDao ) AppUtil . getApplicationContext (). getBean ( "environmentVariableDao" ); // Load target environment variable by ID EnvironmentVariable envVar = envDao . loadById ( variableId , appDef ); if ( envVar != null ) { LogUtil . info ( "EnvVar Manager
AI 资讯
Custom Cell Renderers & Action Buttons in Joget Spreadsheet Elements
Custom Cell Renderers & Action Buttons in Joget Spreadsheet Elements The built-in Spreadsheet Element in Joget DX provides a spreadsheet-like interface for managing tabular records inside forms. However, standard spreadsheet columns only support basic text or dropdown inputs out of the box. If you want to add row-level action buttons (like a Delete Row button) or turn plain cell text into an interactive Modal Popup Link , you can supply custom Handsontable renderer functions directly inside your Spreadsheet column properties. In this guide, we'll look at two practical examples: adding a custom row-deletion button and rendering interactive drill-down links. Example 1: Adding a Custom Delete Row Button In your Joget Spreadsheet element, open column properties for an action column and configure the custom renderer function below: {{ renderer : function ( instance , td , row , col , prop , value , cellProperties ) { // Render custom HTML button inside the cell td . innerHTML = " <button type='button' class='btn-delete-row'>Delete</button> " ; td . style . textAlign = " center " ; // Attach click handler to remove the target row from the Handsontable instance const btn = td . querySelector ( " .btn-delete-row " ); btn . onclick = function ( e ) { e . preventDefault (); e . stopPropagation (); // Get underlying Handsontable instance from the form field const hotInstance = FormUtil . getField ( " your_spreadsheet_field_id " ). data ( " hot " ); if ( hotInstance ) { hotInstance . alter ( " remove_row " , row ); } }; } }} Key Highlights: instance.alter("remove_row", row) removes the target row directly from the underlying data model. e.stopPropagation() prevents Handsontable from entering cell-edit mode when the button is clicked. Example 2: Interactive Drill-Down Popup Links To display a clickable link in a grid cell that opens a detailed record inside a Joget modal dialog (popup iframe), use this cell renderer: {{ renderer : function ( instance , td , row , col , prop , va
开发者
How an Unindexed Column Silently Killed Our Database under Load (and the 5-Minute Fix)
How a Missing Database Index Turned an 8ms API into an 8-Second Nightmare Every developer...
开发者
How I Made JavaScript Execution Visual and Rewindable (DSA View View 👀👀)
Hoi hoi! I'm @nyaomaru, a frontend engineer who was recently knocked off my feet by how good the...