<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Anubhav singh on Medium]]></title>
        <description><![CDATA[Stories by Anubhav singh on Medium]]></description>
        <link>https://medium.com/@chauhananubhav16?source=rss-84db070f5373------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*HALPayZ89Llsynpz.jpg</url>
            <title>Stories by Anubhav singh on Medium</title>
            <link>https://medium.com/@chauhananubhav16?source=rss-84db070f5373------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Tue, 19 May 2026 18:44:56 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@chauhananubhav16/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[We Translated 50,000 Strings Into 30 Languages Using AI — Here’s Our Exact Pipeline]]></title>
            <link>https://medium.com/@chauhananubhav16/we-translated-50-000-strings-into-30-languages-using-ai-heres-our-exact-pipeline-dfc1a924c44c?source=rss-84db070f5373------2</link>
            <guid isPermaLink="false">https://medium.com/p/dfc1a924c44c</guid>
            <category><![CDATA[openai]]></category>
            <category><![CDATA[i18n]]></category>
            <category><![CDATA[translation]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <category><![CDATA[javascript]]></category>
            <dc:creator><![CDATA[Anubhav singh]]></dc:creator>
            <pubDate>Mon, 11 May 2026 06:29:13 GMT</pubDate>
            <atom:updated>2026-05-11T06:29:13.411Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*poa2HdQG3MFZCjfSOT5XAg.png" /><figcaption>We Translated 50,000 Strings Into 30 Languages Using AI — Here’s Our Exact Pipeline</figcaption></figure><p><em>How we built an automated translation pipeline that extracts strings from code, sends them to GPT-4o-mini in smart batches, and writes back to PO files — with full code.</em></p><p>Our app supports 30+ languages. That’s over 50,000 translatable strings — UI labels, error messages, SEO descriptions, and full blog posts. Maintaining translations manually with Crowdin or freelance translators was costing us $2,000/month and taking 2–3 weeks per release cycle. We replaced 90% of that workflow with a single Node.js script that calls GPT-4o-mini. Here’s exactly how it works.</p><h3>The Problem With Traditional Translation Workflows</h3><p>Our stack uses <a href="https://lingui.dev">Lingui.js</a> for internationalization. The workflow looks like this: developers write code with t`Hello world` macros, run lingui extract to pull strings into PO files, then send those PO files to translators. The translators fill in the msgstr fields, we pull the files back, compile them, and ship.</p><p>The bottleneck was obvious. Every new feature added 20–50 new strings. Each string needed translation into 30 languages. That’s 600–1,500 translation tasks per feature. Human translators are accurate but slow — a typical turnaround was 5–7 business days. For a fast-moving product, that meant features shipped in English first and other languages caught up weeks later. Not acceptable for a global product.</p><h3>The Architecture: Extract → Batch → Translate → Write</h3><p>Our pipeline has four stages:</p><ol><li><strong>Extract:</strong> Lingui CLI scans source code and generates PO files with empty msgstr fields for new strings.</li><li><strong>Classify &amp; Batch:</strong> We group untranslated strings by length — short UI labels get batched aggressively (40 per API call), medium paragraphs go in groups of 10, and long HTML content (blog posts) goes one at a time.</li><li><strong>Translate:</strong> Each batch hits GPT-4o-mini with a domain-specific system prompt that understands our product terminology.</li><li><strong>Write:</strong> Translations are written back to the PO files using gettext-parser. Progress is saved after every batch so nothing is lost on failure.</li></ol><h3>Step 1: Extraction With Lingui</h3><p>Lingui makes extraction trivial. In your code, you write:</p><pre>import { t } from &quot;@lingui/core/macro&quot;;<br>const label = t`Upload your resume`;<br>const greeting = t`Hello, ${name}!`;</pre><p>Then run the extract command:</p><pre>pnpm exec lingui extract --clean --overwrite</pre><p>This scans every file matching your Lingui config, finds all t`...` and msg`...` calls, and generates (or updates) PO files for each locale. A PO entry looks like this:</p><pre>#: src/components/upload.tsx:14<br>msgid &quot;Upload your resume&quot;<br>msgstr &quot;&quot;</pre><pre>#: src/components/greeting.tsx:8<br>msgid &quot;Hello, {0}!&quot;<br>msgstr &quot;&quot;</pre><p>The msgstr &quot;&quot; means untranslated. Our script finds these and fills them in.</p><h3>Step 2: Smart Batching by String Length</h3><p>Not all strings are equal. Sending “Cancel” and a 2,000-word blog post in the same API call is wasteful and error-prone. We classify strings into three tiers:</p><pre>const TIERS = {<br>  short: { maxChars: 200, batchSize: 40 }, // &quot;Save&quot;, &quot;Cancel&quot;<br>  medium: { maxChars: 1000, batchSize: 10 }, // Paragraphs<br>  long: { maxChars: Infinity, batchSize: 1 }, // Blog HTML<br>};</pre><p>Short strings batch aggressively — 40 strings per API call. The model handles these perfectly because they’re simple, context-free labels. Medium strings (descriptions, tooltips) go in groups of 10. Long strings (full blog posts with HTML) always go solo to avoid line-count mismatches in the response.</p><p>We also cap each batch at a token budget to avoid hitting model limits:</p><pre>const MAX_TOKENS_PER_BATCH = 3000;<br>function estimateTokens(text) {<br>  // Conservative: 1 token per 3 chars (accounts for non-English expansion)<br>  return Math.ceil(text.length / 3);<br>}<br>function buildTokenAwareBatches(strings, maxBatchSize) {<br>  const batches = [];<br>  let currentBatch = [];<br>  let currentTokens = 0;<br>  for (const text of strings) {<br>    const tokens = estimateTokens(text);<br>    if (<br>      currentBatch.length &gt; 0 &amp;&amp;<br>      (currentTokens + tokens &gt; MAX_TOKENS_PER_BATCH || currentBatch.length &gt;= maxBatchSize)<br>    ) {<br>      batches.push({ texts: [...currentBatch], mode: &quot;batch&quot; });<br>      currentBatch = [];<br>      currentTokens = 0;<br>    }<br>    currentBatch.push(text);<br>    currentTokens += tokens;<br>  }<br>  if (currentBatch.length &gt; 0) {<br>    batches.push({ texts: [...currentBatch], mode: &quot;batch&quot; });<br>  }<br>  return batches;<br>}</pre><h3>Step 3: The Translation Prompt</h3><p>The system prompt is everything. A generic “translate this” prompt produces generic translations. Our prompt is domain-specific:</p><pre>const SYSTEM_PROMPT = `You are a professional translator for MyLiveCV — an online<br>resume builder, cover letter creator, portfolio maker, and job application tracker.<br>Your task:<br>1. Translate each line into the target language naturally.<br>2. Follow PO file syntax - return ONLY translated strings, one per line, in the same order.<br>3. Preserve ALL placeholders: {0}, {1}, &lt;0&gt;, &lt;/0&gt;, {name}, {count}<br>4. Preserve HTML tags: &lt;strong&gt;, &lt;em&gt;, &lt;br/&gt;, etc.<br>5. Use locale-appropriate SEO keywords for resume/CV terms.<br>6. Keep untranslatable terms as-is: &quot;PDF&quot;, &quot;ATS&quot;, &quot;JSON Resume&quot;<br>7. Return exactly the same number of lines as input.<br>8. NEVER convert square brackets [] to curly braces {}.`;</pre><p>Key decisions:</p><ul><li><strong>Locale-appropriate SEO keywords</strong> — “resume” in French is “CV” and in German it’s “Lebenslauf.” A literal translation would hurt our SEO.</li><li><strong>Placeholder protection</strong> — without this instruction, the model occasionally converts {0} to a translated word.</li><li><strong>Square bracket rule</strong> — we learned this the hard way. The model kept converting [your name here] to {your name here}, breaking our fill-in-the-blank text.</li></ul><h3>Step 4: Batch Translation With Fallback</h3><p>The batch translation function sends numbered lines and expects numbered translations back:</p><pre>async function translateBatch(texts, targetLang, retries = 2) {<br>  const userContent = texts.map((t, i) =&gt; `${i + 1}. ${t}`).join(&quot;\n&quot;);<br>  for (let attempt = 0; attempt &lt;= retries; attempt++) {<br>      try {<br>        const response = await openai.chat.completions.create({<br>          model: &quot;gpt-4o-mini&quot;,<br>          temperature: 0.3,<br>          messages: [<br>            { role: &quot;system&quot;, content: `${SYSTEM_PROMPT}\n\nTarget language: ${targetLang}` },<br>            {<br>              role: &quot;user&quot;,<br>              content: `Translate these ${texts.length} lines. Return ONLY the translations, one per line, numbered to match:\n\n${userContent}`,<br>            },<br>          ],<br>        });<br>        const raw = response.choices[0].message.content.trim();<br>        const lines = raw<br>          .split(&quot;\n&quot;)<br>          .map((line) =&gt; line.replace(/^\d+\.\s*/, &quot;&quot;).trim())<br>          .filter((line) =&gt; line.length &gt; 0);<br>        if (lines.length === texts.length) return lines;<br>        // Line count mismatch - retry<br>        if (attempt === retries) return null;<br>      } catch (error) {<br>        if (attempt === retries) return null;<br>        await sleep(1000 * (attempt + 1));<br>      }<br>    }<br>    return null;<br>}</pre><p>If the batch returns the wrong number of lines (it happens ~5% of the time with medium batches), we fall back to translating each string individually. This fallback is slower but guarantees correctness:</p><pre>async function translateSingle(text, targetLang) {<br>  const response = await openai.chat.completions.create({<br>    model: &quot;gpt-4o-mini&quot;,<br>    temperature: 0.3,<br>    messages: [<br>      { role: &quot;system&quot;, content: `${SINGLE_SYSTEM_PROMPT}\n\nTarget language: ${targetLang}` },<br>      { role: &quot;user&quot;, content: `Translate this text:\n\n${text}` },<br>    ],<br>  });<br>  return response.choices[0].message.content.trim();<br>}</pre><h3>Step 5: Writing Back to PO Files</h3><p>We use gettext-parser to read and write PO files. After each batch completes, we write immediately — so if the script crashes at string 847 of 1,200, you keep the first 847 translations:</p><pre>const gettextParser = require(&quot;gettext-parser&quot;);<br>const fs = require(&quot;fs&quot;);<br><br>async function translatePoFile(targetLocale, localesDir) {<br>  const poFilePath = `${localesDir}/${targetLocale}/messages.po`;<br>  const content = fs.readFileSync(poFilePath, &quot;utf-8&quot;);<br>  const parsedPo = gettextParser.po.parse(content);<br>  // Find untranslated entries<br>  const untranslated = [];<br>  for (const msgid in parsedPo.translations[&quot;&quot;]) {<br>    const entry = parsedPo.translations[&quot;&quot;][msgid];<br>    if (entry &amp;&amp; msgid &amp;&amp; !entry.msgstr[0]?.trim()) {<br>      untranslated.push(msgid);<br>    }<br>  }<br>  if (untranslated.length === 0) {<br>    console.log(`  ✓ ${targetLocale} - all translated`);<br>    return;<br>  }<br>  // Build smart batches<br>  const { batches } = buildSmartBatches(untranslated);<br>  for (const batch of batches) {<br>    if (batch.mode === &quot;single&quot;) {<br>      const translated = await translateSingle(batch.texts[0], targetLocale);<br>      parsedPo.translations[&quot;&quot;][batch.texts[0]].msgstr = [translated];<br>    } else {<br>      let translations = await translateBatch(batch.texts, targetLocale);<br>      if (translations) {<br>        for (let i = 0; i &lt; batch.texts.length; i++) {<br>          parsedPo.translations[&quot;&quot;][batch.texts[i]].msgstr = [translations[i]];<br>        }<br>      } else {<br>        // Batch failed - fall back to single<br>        for (const msgid of batch.texts) {<br>          const translated = await translateSingle(msgid, targetLocale);<br>          parsedPo.translations[&quot;&quot;][msgid].msgstr = [translated];<br>        }<br>      }<br>    }<br>    // Save after every batch - progress is never lost<br>    const buffer = gettextParser.po.compile(parsedPo);<br>    fs.writeFileSync(poFilePath, buffer.toString(&quot;utf-8&quot;));<br>  }<br>}</pre><h3>Step 6: Language Concurrency</h3><p>With 30 languages, sequential processing would take hours. We run 5 languages in parallel using a simple concurrency limiter:</p><pre>async function runWithConcurrency(tasks, limit) {<br>  const executing = new Set();<br>  for (const task of tasks) {<br>    const p = task().finally(() =&gt; executing.delete(p));<br>    executing.add(p);<br>    if (executing.size &gt;= limit) {<br>      await Promise.race(executing);<br>    }<br>  }<br>  await Promise.all(executing);<br>}<br><br>// Translate 5 languages simultaneously<br>await runWithConcurrency(<br>  languages.map((lang) =&gt; () =&gt; translatePoFile(lang.locale, localesDir)),<br>  5,<br>);</pre><p>Why 5 and not 30? OpenAI rate limits. At 5 concurrent languages with 3 concurrent batches each, we stay well under the TPM (tokens per minute) ceiling for GPT-4o-mini. Bump it higher and you’ll start hitting 429s.</p><h3>The Full Workflow</h3><p>Our package.json has three scripts that chain together:</p><pre>{<br>  &quot;messages:extract&quot;: &quot;lingui extract --clean --overwrite&quot;,<br>  &quot;messages:compile&quot;: &quot;lingui compile&quot;,<br>  &quot;messages:translate&quot;: &quot;node ./tools/translator/openai.js --target=all&quot;<br>}</pre><p>The full workflow after adding new translatable strings:</p><pre># 1. Extract new strings from source code into PO files<br>pnpm messages:extract<br><br># 2. AI-translate all untranslated strings across all locales<br>pnpm messages:translate<br><br># 3. Compile PO files into optimized JS catalogs for runtime<br>pnpm messages:compile</pre><p>That’s it. Three commands. From code change to 30 fully translated languages in about 15 minutes.</p><h3>Results: Cost, Speed, Quality</h3><p><strong>Cost:</strong> GPT-4o-mini costs roughly $0.15 per 1M input tokens and $0.60 per 1M output tokens. Translating 1,000 new strings into 30 languages costs about $2–4. Our monthly translation bill went from $2,000 to under $50.</p><p><strong>Speed:</strong> A full translation run (all untranslated strings across all locales) takes 10–20 minutes. Compare that to 5–7 business days with human translators. Features now ship fully localized on day one.</p><p><strong>Quality:</strong> We spot-check translations with native speakers on our team. Accuracy is around 92–95% for UI strings and 85–90% for long-form content. The remaining issues are usually stylistic preferences, not errors. For a product where the UI text is short and domain-specific, this is more than acceptable.</p><h3>Edge Cases We Handle</h3><p><strong>HTML preservation:</strong> Blog posts contain full HTML markup. The prompt explicitly instructs the model to preserve tags. We validate that the output has the same number of opening and closing tags as the input.</p><p><strong>Placeholder protection:</strong> Strings like Hello, {0}! must keep {0} intact. We check post-translation that all placeholders from the source appear in the output. If any are missing, we retry that string individually.</p><p><strong>Smart quotes:</strong> Some models return curly quotes (“ “) instead of straight quotes. We run a post-processing pass that normalizes these back to ASCII quotes, which PO parsers expect:</p><pre>function cleanTranslation(text) {<br>  return text<br>    .replace(/\\&quot;/g, &#39;&quot;&#39;)<br>    .replace(/^&quot;|&quot;$/g, &quot;&quot;)<br>    .replace(/\u201C/g, &#39;&quot;&#39;)<br>    .replace(/\u201D/g, &#39;&quot;&#39;);<br>}</pre><p><strong>Right-to-left languages:</strong> Arabic, Hebrew, and Farsi work fine — the model handles RTL text natively. The PO file format is encoding-agnostic so no special handling is needed on our side.</p><h3>Post-Processing: Fixing Broken Quotes</h3><p>One issue we hit repeatedly: the AI sometimes wraps translations in quotes or escapes internal quotes inconsistently. We wrote a separate fix-quotes.js script that runs after translation:</p><pre>// Strips wrapping quotes and fixes escaped quotes in PO msgstr values<br>for (const msgid in parsedPo.translations[&quot;&quot;]) {<br>  const entry = parsedPo.translations[&quot;&quot;][msgid];<br>  if (!entry.msgstr[0]) continue;<br>  entry.msgstr[0] = entry.msgstr[0]<br>    .replace(/^&quot;(.*)&quot;$/s, &quot;$1&quot;) // Remove wrapping quotes<br>    .replace(/\\&quot;/g, &#39;&quot;&#39;); // Unescape internal quotes<br>}</pre><p>This runs as a separate pass (pnpm messages:fix-quotes) and catches the ~2% of strings where the model adds unnecessary escaping.</p><h3>When NOT to Use AI Translation</h3><p>Legal text, medical content, and anything where a mistranslation has real consequences. For a resume builder, the worst case is a slightly awkward button label — annoying but not dangerous. If your product handles financial transactions or health data, keep human translators for critical paths and use AI for the low-risk UI strings.</p><p>Also: <strong>do not skip the compile step.</strong> Lingui compiles PO files into optimized JavaScript catalogs that load at runtime. Shipping raw PO files to the browser would be absurdly slow. The compile step is what makes this production-ready.</p><h3>The Bottom Line</h3><p>AI translation for PO files is not a future thing — it works today, right now, with a 300-line Node.js script. The combination of Lingui’s extraction (which gives you clean source strings), smart batching (which keeps API costs low), and GPT-4o-mini (which handles 30 languages accurately) means you can ship a fully internationalized product without a translation budget.</p><p>Extract, translate, compile, ship. Every release, every language, same day.</p><p><em>We’re building </em><a href="https://mylivecv.com"><em>MyLiveCV</em></a><em> — a resume builder, cover letter creator, portfolio maker, and job tracker. If you’re solving i18n at scale, we’d love to hear how you’re doing it.</em></p><h3>Appendix: Full Script</h3><p>Here’s the complete, production-ready script. Drop it into your project and run it.</p><h3>.env</h3><pre># OpenAI API key (required)<br>OPEN_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxx<br># Model to use (optional, defaults to gpt-4o-mini)<br>OPEN_API_MODEL=gpt-4o-mini</pre><h3>languages.js</h3><pre>// languages.js — define your target locales<br>const languages = [<br>  { id: &quot;ar&quot;, name: &quot;Arabic&quot;, locale: &quot;ar-SA&quot; },<br>  { id: &quot;bn&quot;, name: &quot;Bengali&quot;, locale: &quot;bn-BD&quot; },<br>  { id: &quot;zh&quot;, name: &quot;Chinese Simplified&quot;, locale: &quot;zh-CN&quot; },<br>  { id: &quot;nl&quot;, name: &quot;Dutch&quot;, locale: &quot;nl-NL&quot; },<br>  { id: &quot;en&quot;, name: &quot;English&quot;, locale: &quot;en-US&quot; },<br>  { id: &quot;fr&quot;, name: &quot;French&quot;, locale: &quot;fr-FR&quot; },<br>  { id: &quot;de&quot;, name: &quot;German&quot;, locale: &quot;de-DE&quot; },<br>  { id: &quot;hi&quot;, name: &quot;Hindi&quot;, locale: &quot;hi-IN&quot; },<br>  { id: &quot;hu&quot;, name: &quot;Hungarian&quot;, locale: &quot;hu-HU&quot; },<br>  { id: &quot;id&quot;, name: &quot;Indonesian&quot;, locale: &quot;id-ID&quot; },<br>  { id: &quot;it&quot;, name: &quot;Italian&quot;, locale: &quot;it-IT&quot; },<br>  { id: &quot;ja&quot;, name: &quot;Japanese&quot;, locale: &quot;ja-JP&quot; },<br>  { id: &quot;ko&quot;, name: &quot;Korean&quot;, locale: &quot;ko-KR&quot; },<br>  { id: &quot;pl&quot;, name: &quot;Polish&quot;, locale: &quot;pl-PL&quot; },<br>  { id: &quot;pt&quot;, name: &quot;Portuguese&quot;, locale: &quot;pt-PT&quot; },<br>  { id: &quot;ru&quot;, name: &quot;Russian&quot;, locale: &quot;ru-RU&quot; },<br>  { id: &quot;es&quot;, name: &quot;Spanish&quot;, locale: &quot;es-ES&quot; },<br>  { id: &quot;th&quot;, name: &quot;Thai&quot;, locale: &quot;th-TH&quot; },<br>  { id: &quot;tr&quot;, name: &quot;Turkish&quot;, locale: &quot;tr-TR&quot; },<br>  { id: &quot;vi&quot;, name: &quot;Vietnamese&quot;, locale: &quot;vi-VN&quot; },<br>];<br><br>module.exports = { languages };</pre><h3>translate.js — The Full Script</h3><pre>const fs = require(&quot;fs&quot;);<br>const dotenv = require(&quot;dotenv&quot;);<br>const path = require(&quot;path&quot;);<br>const OpenAI = require(&quot;openai&quot;);<br>const { languages } = require(&quot;./languages.js&quot;);<br><br>dotenv.config();<br><br>const OPENAI_API_KEY = process.env.OPEN_API_KEY;<br>const OPEN_API_MODEL = process.env.OPEN_API_MODEL || &quot;gpt-4o-mini&quot;;<br><br>const LOCALE_PATHS = {<br>  client: &quot;apps/client/src/locales&quot;,<br>  frontend: &quot;apps/frontend/src/locales&quot;,<br>};<br><br>// Parse CLI arguments<br>const targetArg = process.argv.find((a) =&gt; a.startsWith(&quot;--target=&quot;));<br>const target = targetArg ? targetArg.split(&quot;=&quot;)[1] : &quot;client&quot;;<br>const concurrencyArg = process.argv.find((a) =&gt; a.startsWith(&quot;--concurrency=&quot;));<br>const LANG_CONCURRENCY = concurrencyArg ? parseInt(concurrencyArg.split(&quot;=&quot;)[1], 10) : 5;<br><br>// ── Smart batching thresholds ─────────────────────────────────────────────────<br>// Strings are classified by character length into tiers with different batch sizes.<br>// This saves tokens by batching short UI strings aggressively while sending<br>// long content (blogs, HTML blocks) one-at-a-time to avoid line-count mismatches.<br><br>const TIERS = {<br>  short: { maxChars: 200, batchSize: 40 }, // &quot;Save&quot;, &quot;Cancel&quot;, short sentences<br>  medium: { maxChars: 1000, batchSize: 10 }, // Paragraphs, descriptions<br>  long: { maxChars: Infinity, batchSize: 1 }, // Blog HTML, multi-paragraph content<br>};<br><br>// Rough token estimate: ~4 chars per token for English, ~2-3 for CJK.<br>// We cap each batch at this token budget to avoid hitting model limits.<br>const MAX_TOKENS_PER_BATCH = 3000;<br><br>function estimateTokens(text) {<br>  // Conservative: 1 token per 3 chars (accounts for non-English expansion)<br>  return Math.ceil(text.length / 3);<br>}<br><br>function classifyString(text) {<br>  const len = text.length;<br>  if (len &lt;= TIERS.short.maxChars) return &quot;short&quot;;<br>  if (len &lt;= TIERS.medium.maxChars) return &quot;medium&quot;;<br>  return &quot;long&quot;;<br>}<br><br>// PO files to translate per target. The translator processes each file independently.<br>const PO_FILES = {<br>  client: [&quot;messages.po&quot;],<br>  frontend: [&quot;messages.po&quot;, &quot;blogs.po&quot;],<br>};<br><br>if (target !== &quot;client&quot; &amp;&amp; target !== &quot;frontend&quot; &amp;&amp; target !== &quot;all&quot;) {<br>  console.error(&quot;Error: --target must be &#39;client&#39;, &#39;frontend&#39;, or &#39;all&#39;&quot;);<br>  process.exit(1);<br>}<br><br>const targets = target === &quot;all&quot; ? [&quot;client&quot;, &quot;frontend&quot;] : [target];<br><br>if (!OPENAI_API_KEY) {<br>  console.error(&quot;Error: Missing OpenAI API Key. Set OPEN_API_KEY in your .env file.&quot;);<br>  process.exit(1);<br>}<br><br>const openai = new OpenAI({ apiKey: OPENAI_API_KEY });<br><br>// ─── System Prompt ────────────────────────────────────────────────────────────<br><br>const SYSTEM_PROMPT = `You are a professional translator for MyLiveCV — an online resume builder, cover letter creator, portfolio maker, and job application tracker.<br><br>Your task:<br>1. Translate each line into the target language. Keep it natural, fluent, and human-readable.<br>2. Follow PO file syntax strictly — return ONLY the translated strings, one per line, in the same order as the input.<br>3. Preserve ALL placeholders exactly: {0}, {1}, &lt;0&gt;, &lt;/0&gt;, &lt;1&gt;, &lt;/1&gt;, {name}, {count}, etc.<br>4. Preserve HTML tags if present: &lt;strong&gt;, &lt;em&gt;, &lt;br/&gt;, etc.<br>5. Use locale-appropriate SEO keywords for resume/CV, cover letter, portfolio, and job search terms.<br>6. If a term has no natural translation (e.g., &quot;PDF&quot;, &quot;ATS&quot;, &quot;JSON Resume&quot;), keep the original.<br>7. Do NOT add quotes, escape characters, explanations, or numbering. Return raw translated text only.<br>8. If you cannot translate a line, return it unchanged.<br>9. Return exactly the same number of lines as the input — one translation per line.<br>10. NEVER convert square brackets [] to curly braces {}. Square brackets like [specific ask] or [your name here] are fill-in-the-blank text, NOT placeholders. Translate the text inside and keep the square brackets.<br><br>Domain context: MyLiveCV helps users create professional resumes/CVs, cover letters, portfolios, and track job applications. Common terms: resume, CV, cover letter, portfolio, template, ATS score, job tracker, PDF export, AI assistant.`;<br><br>const SINGLE_SYSTEM_PROMPT = `You are a professional translator for MyLiveCV — an online resume builder, cover letter creator, portfolio maker, and job application tracker.<br><br>Your task:<br>1. Translate the given text into the target language. Keep it natural, fluent, and human-readable.<br>2. Preserve ALL placeholders exactly: {0}, {1}, &lt;0&gt;, &lt;/0&gt;, &lt;1&gt;, &lt;/1&gt;, {name}, {count}, etc.<br>3. Preserve HTML tags and structure if present: &lt;strong&gt;, &lt;em&gt;, &lt;br/&gt;, &lt;div&gt;, &lt;section&gt;, &lt;h2&gt;, &lt;ul&gt;, &lt;li&gt;, etc.<br>4. Use locale-appropriate SEO keywords for resume/CV, cover letter, portfolio, and job search terms.<br>5. If a term has no natural translation (e.g., &quot;PDF&quot;, &quot;ATS&quot;, &quot;JSON Resume&quot;), keep the original.<br>6. Do NOT add quotes, escape characters, explanations, or numbering. Return ONLY the translated text.<br>7. If you cannot translate the text, return it unchanged.<br>8. NEVER convert square brackets [] to curly braces {}. Square brackets like [specific ask] are fill-in-the-blank text, NOT placeholders. Translate the text inside and keep the square brackets.<br><br>Domain context: MyLiveCV helps users create professional resumes/CVs, cover letters, portfolios, and track job applications.`;<br><br>// ─── Translate a batch of short/medium strings ────────────────────────────────<br><br>async function translateBatch(texts, targetLang, retries = 2) {<br>  const userContent = texts.map((t, i) =&gt; `${i + 1}. ${t}`).join(&quot;\n&quot;);<br><br>  for (let attempt = 0; attempt &lt;= retries; attempt++) {<br>    try {<br>      const params = {<br>        model: OPEN_API_MODEL,<br>        messages: [<br>          { role: &quot;system&quot;, content: `${SYSTEM_PROMPT}\n\nTarget language: ${targetLang}` },<br>          {<br>            role: &quot;user&quot;,<br>            content: `Translate these ${texts.length} lines. Return ONLY the translations, one per line, numbered to match:\n\n${userContent}`,<br>          },<br>        ],<br>      };<br><br>      let response;<br>      try {<br>        response = await openai.chat.completions.create({ ...params, temperature: 0.3 });<br>      } catch (tempError) {<br>        if (tempError.message?.includes(&quot;temperature&quot;)) {<br>          response = await openai.chat.completions.create(params);<br>        } else {<br>          throw tempError;<br>        }<br>      }<br><br>      const raw = response.choices[0].message.content.trim();<br><br>      // Parse numbered lines: &quot;1. translation&quot; → [&quot;translation&quot;, ...]<br>      const lines = raw<br>        .split(&quot;\n&quot;)<br>        .map((line) =&gt; line.replace(/^\d+\.\s*/, &quot;&quot;).trim())<br>        .filter((line) =&gt; line.length &gt; 0);<br><br>      if (lines.length === texts.length) {<br>        return lines.map(cleanTranslation);<br>      }<br><br>      if (attempt === retries) {<br>        console.warn(<br>          `    ⚠ Batch line count mismatch (got ${lines.length}, expected ${texts.length}), falling back to single mode`,<br>        );<br>        return null;<br>      }<br>    } catch (error) {<br>      if (attempt === retries) {<br>        console.error(`    ✗ Batch translation failed: ${error.message}`);<br>        return null;<br>      }<br>      await sleep(1000 * (attempt + 1));<br>    }<br>  }<br>  return null;<br>}<br><br>// ─── Translate a single long string (blog, HTML block, etc.) ──────────────────<br><br>async function translateSingle(text, targetLang) {<br>  try {<br>    const params = {<br>      model: OPEN_API_MODEL,<br>      messages: [<br>        { role: &quot;system&quot;, content: `${SINGLE_SYSTEM_PROMPT}\n\nTarget language: ${targetLang}` },<br>        { role: &quot;user&quot;, content: `Translate this text:\n\n${text}` },<br>      ],<br>    };<br><br>    let response;<br>    try {<br>      response = await openai.chat.completions.create({ ...params, temperature: 0.3 });<br>    } catch (tempError) {<br>      if (tempError.message?.includes(&quot;temperature&quot;)) {<br>        response = await openai.chat.completions.create(params);<br>      } else {<br>        throw tempError;<br>      }<br>    }<br><br>    return cleanTranslation(response.choices[0].message.content.trim());<br>  } catch (error) {<br>    console.error(`    ✗ Single translation failed: ${error.message}`);<br>    return text;<br>  }<br>}<br><br>function cleanTranslation(text) {<br>  return text<br>    .replace(/\\&quot;/g, &#39;&quot;&#39;)<br>    .replace(/^&quot;|&quot;$/g, &quot;&quot;)<br>    .replace(/\u201C/g, &#39;&quot;&#39;)<br>    .replace(/\u201D/g, &#39;&quot;&#39;);<br>}<br><br>function sleep(ms) {<br>  return new Promise((resolve) =&gt; setTimeout(resolve, ms));<br>}<br><br>// ─── Smart batching: group strings by size tier, then split by token budget ───<br><br>function buildSmartBatches(strings) {<br>  // Classify each string<br>  const classified = strings.map((s) =&gt; ({ text: s, tier: classifyString(s) }));<br><br>  // Separate by tier<br>  const shortStrings = classified.filter((c) =&gt; c.tier === &quot;short&quot;).map((c) =&gt; c.text);<br>  const mediumStrings = classified.filter((c) =&gt; c.tier === &quot;medium&quot;).map((c) =&gt; c.text);<br>  const longStrings = classified.filter((c) =&gt; c.tier === &quot;long&quot;).map((c) =&gt; c.text);<br><br>  const batches = [];<br><br>  // Build batches for short strings — large batches, capped by token budget<br>  batches.push(...buildTokenAwareBatches(shortStrings, TIERS.short.batchSize));<br><br>  // Build batches for medium strings — smaller batches, capped by token budget<br>  batches.push(...buildTokenAwareBatches(mediumStrings, TIERS.medium.batchSize));<br><br>  // Long strings always go single (batch of 1)<br>  for (const text of longStrings) {<br>    batches.push({ texts: [text], mode: &quot;single&quot; });<br>  }<br><br>  return {<br>    batches,<br>    stats: { short: shortStrings.length, medium: mediumStrings.length, long: longStrings.length },<br>  };<br>}<br><br>function buildTokenAwareBatches(strings, maxBatchSize) {<br>  const batches = [];<br>  let currentBatch = [];<br>  let currentTokens = 0;<br><br>  for (const text of strings) {<br>    const tokens = estimateTokens(text);<br><br>    // If adding this string would exceed the token budget or batch size, flush<br>    if (<br>      currentBatch.length &gt; 0 &amp;&amp;<br>      (currentTokens + tokens &gt; MAX_TOKENS_PER_BATCH || currentBatch.length &gt;= maxBatchSize)<br>    ) {<br>      batches.push({ texts: [...currentBatch], mode: &quot;batch&quot; });<br>      currentBatch = [];<br>      currentTokens = 0;<br>    }<br><br>    currentBatch.push(text);<br>    currentTokens += tokens;<br>  }<br><br>  if (currentBatch.length &gt; 0) {<br>    batches.push({ texts: [...currentBatch], mode: &quot;batch&quot; });<br>  }<br><br>  return batches;<br>}<br><br>// ─── PO File I/O ──────────────────────────────────────────────────────────────<br><br>async function readPoFile(filePath) {<br>  const content = fs.readFileSync(filePath, &quot;utf-8&quot;);<br>  const gettextParser = await import(&quot;gettext-parser&quot;);<br>  return gettextParser.po.parse(content);<br>}<br><br>async function writePoFile(filePath, poData) {<br>  const gettextParser = await import(&quot;gettext-parser&quot;);<br>  const buffer = gettextParser.po.compile(poData);<br>  let content = buffer.toString(&quot;utf-8&quot;);<br>  content = content.replace(/\u201C/g, &#39;&quot;&#39;).replace(/\u201D/g, &#39;&quot;&#39;);<br>  fs.writeFileSync(filePath, content, &quot;utf-8&quot;);<br>}<br><br>// ─── Translate a single locale ────────────────────────────────────────────────<br><br>async function translatePoFileForLanguage(targetLocale, localesDir, poFileName = &quot;messages.po&quot;) {<br>  const poFilePath = path.resolve(`${localesDir}/${targetLocale}/${poFileName}`);<br><br>  try {<br>    const parsedPo = await readPoFile(poFilePath);<br><br>    // Collect untranslated entries<br>    const untranslated = [];<br>    for (const msgid in parsedPo.translations[&quot;&quot;]) {<br>      const entry = parsedPo.translations[&quot;&quot;][msgid];<br>      if (entry &amp;&amp; msgid &amp;&amp; !(entry.msgstr[0] || &quot;&quot;).trim()) {<br>        untranslated.push(msgid);<br>      }<br>    }<br><br>    if (untranslated.length === 0) {<br>      console.log(`    ✓ ${targetLocale} — all translated`);<br>      return;<br>    }<br><br>    // Build smart batches based on string size<br>    const { batches, stats } = buildSmartBatches(untranslated);<br><br>    const batchCount = batches.filter((b) =&gt; b.mode === &quot;batch&quot;).length;<br>    const singleCount = batches.filter((b) =&gt; b.mode === &quot;single&quot;).length;<br>    const totalStrings = untranslated.length;<br>    const estimatedCalls = batchCount + singleCount;<br><br>    console.log(<br>      `    ${targetLocale} — ${totalStrings} strings (${stats.short} short, ${stats.medium} medium, ${stats.long} long) → ${estimatedCalls} API calls (${batchCount} batched, ${singleCount} single)`,<br>    );<br><br>    // Run batches with concurrency of 3<br>    let completed = 0;<br>    await runWithConcurrency(<br>      batches.map((batch) =&gt; async () =&gt; {<br>        if (batch.mode === &quot;single&quot;) {<br>          // Long string — translate individually<br>          const msgid = batch.texts[0];<br>          const translated = await translateSingle(msgid, targetLocale);<br>          parsedPo.translations[&quot;&quot;][msgid].msgstr = [translated];<br>        } else {<br>          // Short/medium batch — try batch mode first<br>          let translations = await translateBatch(batch.texts, targetLocale);<br><br>          if (translations) {<br>            for (let j = 0; j &lt; batch.texts.length; j++) {<br>              parsedPo.translations[&quot;&quot;][batch.texts[j]].msgstr = [translations[j]];<br>            }<br>          } else {<br>            // Batch failed — fall back to single for this batch only<br>            for (const msgid of batch.texts) {<br>              const translated = await translateSingle(msgid, targetLocale);<br>              parsedPo.translations[&quot;&quot;][msgid].msgstr = [translated];<br>            }<br>          }<br>        }<br><br>        completed++;<br>        if (completed % 5 === 0 || completed === batches.length) {<br>          console.log(`    ${targetLocale} [${completed}/${batches.length}] ✓`);<br>        }<br><br>        // Save after each batch so progress isn&#39;t lost<br>        await writePoFile(poFilePath, parsedPo);<br>      }),<br>      3,<br>    );<br><br>    console.log(`    ✓ ${targetLocale} — done`);<br>  } catch (error) {<br>    console.error(`    ✗ ${targetLocale} — ${error.message}`);<br>  }<br>}<br><br>// ─── Concurrency Runner ───────────────────────────────────────────────────────<br><br>async function runWithConcurrency(tasks, limit) {<br>  const executing = new Set();<br>  for (const task of tasks) {<br>    const p = task().finally(() =&gt; executing.delete(p));<br>    executing.add(p);<br>    if (executing.size &gt;= limit) {<br>      await Promise.race(executing);<br>    }<br>  }<br>  await Promise.all(executing);<br>}<br><br>// ─── Main ─────────────────────────────────────────────────────────────────────<br><br>async function main() {<br>  console.log(`Model: ${OPEN_API_MODEL}`);<br>  console.log(<br>    `Smart batching: short (≤${TIERS.short.maxChars} chars → batch ${TIERS.short.batchSize}), medium (≤${TIERS.medium.maxChars} chars → batch ${TIERS.medium.batchSize}), long (&gt;1000 chars → single)`,<br>  );<br>  console.log(`Token budget per batch: ${MAX_TOKENS_PER_BATCH}`);<br>  console.log(`Language concurrency: ${LANG_CONCURRENCY}\n`);<br><br>  for (const t of targets) {<br>    const localesDir = LOCALE_PATHS[t];<br>    const poFiles = PO_FILES[t] || [&quot;messages.po&quot;];<br>    console.log(`── Translating ${t} (${localesDir}) — files: ${poFiles.join(&quot;, &quot;)} ──\n`);<br><br>    const tasks = languages<br>      .filter((lang) =&gt; lang.locale !== &quot;en-US&quot;)<br>      .flatMap((language) =&gt;<br>        poFiles.map((poFileName) =&gt; async () =&gt; {<br>          const poFile = path.resolve(`${localesDir}/${language.locale}/${poFileName}`);<br>          if (!fs.existsSync(poFile)) {<br>            console.log(<br>              `    Skipping ${language.name} (${language.locale}) ${poFileName} — no file`,<br>            );<br>            return;<br>          }<br>          await translatePoFileForLanguage(language.locale, localesDir, poFileName);<br>        }),<br>      );<br><br>    await runWithConcurrency(tasks, LANG_CONCURRENCY);<br>    console.log(`\n✓ ${t} translations completed!\n`);<br>  }<br><br>  console.log(&quot;All translations completed.&quot;);<br>}<br><br>main();</pre><h3>package.json scripts</h3><pre>{<br>  &quot;scripts&quot;: {<br>    &quot;messages:extract&quot;: &quot;lingui extract --clean --overwrite&quot;,<br>    &quot;messages:compile&quot;: &quot;lingui compile&quot;,<br>    &quot;messages:translate&quot;: &quot;node ./tools/translator/translate.js --target=all&quot;,<br>    &quot;messages:fix-quotes&quot;: &quot;node ./tools/translator/fix-quotes.js --target=all&quot;<br>  },<br>  &quot;devDependencies&quot;: {<br>    &quot;gettext-parser&quot;: &quot;^8.0.0&quot;,<br>    &quot;openai&quot;: &quot;^4.104.0&quot;,<br>    &quot;dotenv&quot;: &quot;^16.6.1&quot;<br>  }<br>}</pre><h3>Running It</h3><pre># Install dependencies<br>npm install openai gettext-parser dotenv<br><br># 1. Extract strings from source code<br>npm run messages:extract<br># 2. Translate all untranslated strings<br>npm run messages:translate<br># 3. Fix any smart quote issues<br>npm run messages:fix-quotes<br># 4. Compile to optimized JS catalogs<br>npm run messages:compile</pre><p>Output looks like:</p><pre>🌐 AI PO Translator<br>   Model: gpt-4o-mini<br>   Batching: short ≤200ch → 40/batch, medium ≤1000ch → 10/batch, long → single<br>   Token budget: 3000/batch<br>   Language concurrency: 5<br><br>── frontend (apps/frontend/src/locales) - files: messages.po, blogs.po ──<br>    fr-FR/messages.po - 47 strings (38S 7M 2L) → 6 API calls<br>    fr-FR [3/6] ✓<br>    fr-FR [6/6] ✓<br>    ✓ fr-FR/messages.po - done<br>    de-DE/messages.po - 47 strings (38S 7M 2L) → 6 API calls<br>    ...<br>✓ frontend complete!<br>🎉 All translations done.</pre><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dfc1a924c44c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Big update from MyliveCV.com]]></title>
            <link>https://medium.com/@chauhananubhav16/big-update-from-mylivecv-com-c99ff2c74a6b?source=rss-84db070f5373------2</link>
            <guid isPermaLink="false">https://medium.com/p/c99ff2c74a6b</guid>
            <category><![CDATA[cv]]></category>
            <category><![CDATA[freelancing]]></category>
            <category><![CDATA[resume]]></category>
            <category><![CDATA[ats-software]]></category>
            <category><![CDATA[payments]]></category>
            <dc:creator><![CDATA[Anubhav singh]]></dc:creator>
            <pubDate>Thu, 11 Sep 2025 06:44:02 GMT</pubDate>
            <atom:updated>2025-09-11T06:44:02.501Z</atom:updated>
            <content:encoded><![CDATA[<h3>Big update from MyliveCV.com — 𝗙𝗿𝗲𝗲, 𝗡𝗼 𝗣𝗮𝘆𝘄𝗮𝗹𝗹 + 𝗨𝗻𝗹𝗶𝗺𝗶𝘁𝗲𝗱 𝗗𝗼𝘄𝗻𝗹𝗼𝗮𝗱𝘀!</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*05kV8qnvScFO1lFAypAXUg.png" /></figure><p>We’ve unlocked a powerful free plan for job seekers:<br>✅ 𝟭 𝗙𝗿𝗲𝗲 𝗗𝗼𝗰𝘂𝗺𝗲𝗻𝘁 (Resume / Cover Letter / Portfolio)<br>✅ 𝗨𝗻𝗹𝗶𝗺𝗶𝘁𝗲𝗱 𝗙𝗿𝗲𝗲 𝗗𝗼𝘄𝗻𝗹𝗼𝗮𝗱𝘀 — 𝗻𝗼 𝘄𝗮𝘁𝗲𝗿𝗺𝗮𝗿𝗸𝘀, 𝗻𝗼 𝗹𝗶𝗺𝗶𝘁𝘀<br>✅ Free 𝗔𝗧𝗦 𝗖𝗵𝗲𝗰𝗸 + 𝗥𝗲𝘀𝘂𝗺𝗲 𝗦𝗰𝗼𝗿𝗲<br>✅ Free 𝗝𝗼𝗯 𝗔𝗻𝗮𝗹𝘆𝘀𝗶𝘀 &amp; 𝗧𝗮𝗶𝗹𝗼𝗿𝗲𝗱 𝗦𝘂𝗴𝗴𝗲𝘀𝘁𝗶𝗼𝗻𝘀</p><p>Your career growth shouldn’t be behind a paywall. With MyliveCV, you can build, optimize, and share your resume effortlessly — all 𝗳𝗿𝗲𝗲.</p><p>👉 𝗦𝘁𝗮𝗿𝘁 𝗳𝗿𝗲𝗲 𝗮𝘁 mylivecv.com</p><p>#FreePlan #JobSearch #Resume #CareerGrowth #ATS #MyliveCV #NoPaywall #UnlimitedDownloads</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c99ff2c74a6b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Implementing a Custom Caching Layer in Strapi with Auto Invalidation and Specific Routes Caching]]></title>
            <link>https://medium.com/@chauhananubhav16/implementing-a-custom-caching-layer-in-strapi-with-auto-invalidation-and-specific-routes-caching-35513cbc8f5f?source=rss-84db070f5373------2</link>
            <guid isPermaLink="false">https://medium.com/p/35513cbc8f5f</guid>
            <category><![CDATA[cache]]></category>
            <category><![CDATA[strapi-plugin]]></category>
            <category><![CDATA[strapi-cms]]></category>
            <category><![CDATA[plugins]]></category>
            <category><![CDATA[strapi]]></category>
            <dc:creator><![CDATA[Anubhav singh]]></dc:creator>
            <pubDate>Wed, 27 Aug 2025 14:40:49 GMT</pubDate>
            <atom:updated>2025-08-27T14:40:49.543Z</atom:updated>
            <content:encoded><![CDATA[<blockquote>By default, every request to Strapi goes directly to the database. While that works fine for small projects, high-traffic apps need something smarter: <strong>caching</strong>.</blockquote><blockquote>In this post, we’ll build a <strong>low-code, super-proof caching system for Strapi</strong>. No external plugins. No hidden magic. Just a lightweight, maintainable layer with full control.</blockquote><h3>🚀 Why Custom Cache Instead of Plugins?</h3><p>While there are plugins like strapi-plugin-rest-cache, they often:</p><ul><li>Add extra complexity</li><li>Don’t integrate cleanly with custom logic</li><li>Make debugging harder</li></ul><p>Instead, a <strong>custom caching layer</strong> gives you:</p><ul><li>Fine-grained control over <em>what to cache</em></li><li>Easy invalidation on content changes</li><li>No extra dependency overhead</li></ul><h4>🛠 Step 1: A Simple In-Memory Cache Utility</h4><p>We’ll use a <strong>Map-based in-memory cache</strong> with TTL support:</p><blockquote>create <strong>utils/cache.ts</strong> to keep the utility code logic</blockquote><pre>type CacheItem&lt;T = any&gt; = {<br>  value: T;<br>  expireAt: number;<br>};<br><br>const cache = new Map&lt;string, CacheItem&gt;();<br><br>export const get = &lt;T = any&gt;(key: string): T | null =&gt; {<br>  const item = cache.get(key);<br>  if (!item) return null;<br><br>  if (item.expireAt &lt; Date.now()) {<br>    cache.delete(key);<br>    return null;<br>  }<br><br>  return item.value;<br>};<br><br>export const set = &lt;T = any&gt;(key: string, value: T, ttlSeconds = 60): void =&gt; {<br>  const expireAt = Date.now() + ttlSeconds * 1000;<br>  cache.set(key, { value, expireAt });<br>};<br><br>export const clear = (): void =&gt; cache.clear();<br><br>export const delByPattern = (prefix: string): void =&gt; {<br>  for (const key of cache.keys()) {<br>    if (key.startsWith(prefix)) cache.delete(key);<br>  }<br>};</pre><p>This is lean, fast, and enough for <strong>API response caching</strong>.</p><h4>🛠 Step 2: Strapi Middleware for Caching API Responses</h4><p>We’ll create a middleware that:</p><ul><li>Caches GET requests for configured paths</li><li>Returns cached responses instantly (X-Cache: HIT)</li><li>Stores fresh responses on first request (X-Cache: MISS)</li></ul><blockquote>create <strong>middlewares/cache.ts</strong> to keep the middleware logic</blockquote><pre><br>const config = {<br>    ttl: 900, // 900 seconds = 15 minutes<br>    paths: [&quot;/configurator/&quot;,&quot;/api&quot;], // paths you want to cache<br>  }<br><br>export default () =&gt; {<br>  const cacheablePaths: string[] = config.paths;<br><br>  return async (ctx: any, next: () =&gt; Promise&lt;void&gt;) =&gt; {<br>    const isCacheable =<br>      ctx.request.method === &quot;GET&quot; &amp;&amp;<br>      cacheablePaths.some((prefix) =&gt; ctx.request.url.startsWith(prefix));<br><br>    const key = `cache:${ctx.request.url}`;<br><br>    if (isCacheable) {<br>      const cached = get(key);<br>      if (cached) {<br>        ctx.set(&quot;X-Cache&quot;, &quot;HIT&quot;);<br>        ctx.body = cached;<br>        return;<br>      }<br>    }<br><br>    await next();<br><br>    if (isCacheable &amp;&amp; ctx.status === 200) {<br>      const ttl = config.ttl;<br>      set(key, ctx.body, ttl);<br>      ctx.set(&quot;X-Cache&quot;, &quot;MISS&quot;);<br>    }<br>  };<br>};</pre><p>Now, Strapi will return cached responses for endpoints like /api/blogs or /api/categories.</p><h4>🛠 Step 3: Auto Cache Invalidation on Content Changes</h4><p>Caching is useless if stale data keeps showing. We solve that by hooking into Strapi’s <strong>content lifecycle</strong>.</p><blockquote>Update bootstrap method isnide root index.ts file:</blockquote><pre>import { Core } from &quot;@strapi/strapi&quot;;<br>import { clear } from &quot;./utils/cache&quot;;<br><br>export default {<br>  bootstrap({ strapi }: { strapi: Core.Strapi }) {<br>    strapi.documents.use(async (context, next) =&gt; {<br>      const result = await next();<br>      if ([&quot;publish&quot;, &quot;unpublish&quot;, &quot;delete&quot;].includes(context.action)) {<br>        console.log(<br>          `📢 Cache cleared: ${context.action} triggered on ${context.uid}`<br>        );<br>        clear();<br>      }<br>      return result;<br>    });<br>  },<br>};</pre><p>Now, whenever content is <strong>published, unpublished, or deleted</strong>, the cache is <strong>automatically cleared</strong>.</p><h3>🔑 Key Benefits of This Approach</h3><ul><li><strong>Blazing Fast Responses</strong> — API calls for cached paths return instantly.</li><li><strong>Lightweight &amp; Low-Code</strong> — No heavy plugins, just ~60 lines of logic.</li><li><strong>Auto Invalidation</strong> — No stale content; cache clears on updates.</li><li><strong>Configurable</strong> — Add/remove cacheable paths via environment config.</li></ul><h3>🚀 What’s Next?</h3><p>This in-memory cache works beautifully for <strong>small-to-medium projects</strong>.<br> For production-grade scaling, you can swap the Map store with <strong>Redis</strong> to handle:</p><ul><li>Multiple Strapi pods</li><li>Distributed cache</li><li>Higher memory limits</li></ul><p>But the beauty is: the middleware + utility design remains the same. Just change the storage engine.</p><blockquote>With this, your Strapi API is <strong>faster, leaner, and production-ready</strong> — without third-party plugins.</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=35513cbc8f5f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Bulletproof Full-Text Search (FTS) in Prisma with PostgreSQL tsvector, Without Migration Drift]]></title>
            <link>https://medium.com/@chauhananubhav16/bulletproof-full-text-search-fts-in-prisma-with-postgresql-tsvector-without-migration-drift-c421f63aaab3?source=rss-84db070f5373------2</link>
            <guid isPermaLink="false">https://medium.com/p/c421f63aaab3</guid>
            <category><![CDATA[postgresql]]></category>
            <category><![CDATA[ts-vector]]></category>
            <category><![CDATA[prisma]]></category>
            <category><![CDATA[full-text-search]]></category>
            <category><![CDATA[gin]]></category>
            <dc:creator><![CDATA[Anubhav singh]]></dc:creator>
            <pubDate>Mon, 04 Aug 2025 11:25:35 GMT</pubDate>
            <atom:updated>2025-08-04T11:25:35.372Z</atom:updated>
            <content:encoded><![CDATA[<blockquote>If you’ve tried using PostgreSQL Full-Text Search (tsvector) with Prisma, you probably faced migration drift, schema conflicts, or Prisma removing your GIN indexes.</blockquote><blockquote>Here’s a <strong>step-by-step guide</strong> on how to <strong>use tsvector with Prisma</strong> safely, ensuring Prisma won’t break your setup on future migrations.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HDLVFCWTbNKg_b-D1v9ZDA.png" /></figure><h3>❌ The Problem</h3><p>If you’ve tried this Prisma schema:</p><pre>model JobPost {<br>  id String @id @default(cuid())<br>  description String<br>  description_fts Unsupported(&quot;tsvector GENERATED ALWAYS AS (to_tsvector(&#39;english&#39;, coalesce(description, &#39;&#39;))) STORED&quot;)<br>}</pre><p>You’ll notice a few things break:</p><ul><li>Prisma Migrate will <strong>keep generating DROP/ADD expressions</strong> for the generated column.</li><li>You cannot insert data if Prisma thinks the description_fts field is NULLABLE.</li><li>Prisma’s findMany does not support searching on tsvector columns.</li><li>Your nice <strong>GIN index gets dropped on every </strong><strong>migrate dev</strong>.</li></ul><h3>Problem Recap:</h3><ul><li>Prisma doesn’t handle GENERATED ALWAYS AS columns well.</li><li>Migrate tries to alter/destroy GIN indexes &amp; generated columns.</li><li>Prisma doesn’t support filtering on tsvector fields in findMany.</li></ul><h3>✅ Solution Strategy:</h3><ol><li><strong>Use Prisma </strong><strong>Unsupported(&quot;tsvector&quot;) for schema awareness.</strong></li><li>Allow Prisma to <strong>generate initial migration</strong>.</li><li><strong>Manually drop GENERATED EXPRESSION</strong> (bypasses Prisma limitations).</li><li>Use <strong>PostgreSQL Trigger to sync tsvector updates</strong>.</li><li>Query using <strong>Prisma’s </strong><strong>$queryRaw JOIN</strong> with GIN index benefit.</li></ol><h3>Step 1: Define in Prisma Schema</h3><pre>model Portfolio {<br>  id       String  @id @default(cuid())<br>  data     Json    @default(&quot;{}&quot;)<br>  data_fts Unsupported(&quot;tsvector GENERATED ALWAYS AS (to_tsvector(&#39;english&#39;, coalesce(data::text, &#39;&#39;))) STORED&quot;)? @@index([data_fts], type: Gin)?<br>  @@index([data_fts], type: Gin)<br>}<br></pre><blockquote>Keep data_fts as nullable so it will not conflict with Prisma Client</blockquote><h3>Step 2: Run Prisma Migrate</h3><pre>pnpm prisma migrate dev --name init_fts_setup</pre><p>This will:</p><ul><li>Add the data_fts column as a <strong>GENERATED</strong> column.</li><li>Add a <strong>GIN index</strong>.</li></ul><pre>-- Generated Migration will look like<br><br>-- AlterTable<br>ALTER TABLE &quot;Portfolio&quot; ADD COLUMN     &quot;data_fts&quot; tsvector GENERATED ALWAYS AS (to_tsvector(&#39;english&#39;, coalesce(data::text, &#39;&#39;))) STORED;<br>-- CreateIndex<br>CREATE INDEX &quot;Portfolio_data_fts_idx&quot; ON &quot;Portfolio&quot; USING GIN (&quot;data_fts&quot;);</pre><h3>Step 3: Drop GENERATED EXPRESSION (Manual SQL)</h3><p>After migration, <strong>Prisma will keep re-generating migrations</strong> because of GENERATED columns. To fix this, drop the GENERATED EXPRESSION manually.</p><pre>ALTER TABLE &quot;Portfolio&quot; ALTER COLUMN &quot;data_fts&quot; DROP EXPRESSION;</pre><p>This will:</p><ul><li>Convert the column into a normal <strong>tsvector</strong> column.</li><li>Prisma schema remains intact (since it still expects a tsvector).</li></ul><pre>-- Generated Migration will look like<br><br>-- AlterTable<br>ALTER TABLE &quot;mylivecv&quot;.&quot;Portfolio&quot; ALTER COLUMN &quot;data_fts&quot; DROP EXPRESSION;</pre><h3>Step 4: Create Trigger to Auto-Update data_fts</h3><pre>npx prisma migrate dev --create-only --name ftk triggers</pre><p>This will create migration file, add the trigger script manually</p><pre>CREATE OR REPLACE FUNCTION portfolio_data_fts_trigger() RETURNS trigger AS $$<br>BEGIN<br>    NEW.data_fts := to_tsvector(&#39;english&#39;, COALESCE(NEW.data::text, &#39;&#39;));<br>    RETURN NEW;<br>END;<br>$$ LANGUAGE plpgsql;<br><br><br>CREATE TRIGGER portfolio_data_fts_update<br>BEFORE INSERT OR UPDATE ON &quot;Portfolio&quot;<br>FOR EACH ROW<br>EXECUTE FUNCTION portfolio_data_fts_trigger();</pre><h3>Step 5: Use Prisma Query (Raw)</h3><pre>const searchQuery = &#39;developer&#39;;<br>const portfolios = await prisma.$queryRaw`<br>  SELECT &quot;Portfolio&quot;.*<br>  FROM &quot;Portfolio&quot;<br>  WHERE &quot;Portfolio&quot;.data_fts @@ plainto_tsquery(&#39;english&#39;, ${searchQuery})<br>  ORDER BY ts_rank(&quot;Portfolio&quot;.data_fts, plainto_tsquery(${searchQuery})) DESC;<br>`;</pre><h3><strong>🚀 Why This Method Works:</strong></h3><p><strong>Problem:</strong> Prisma migrate tries to recreate GENERATED column<br> <strong>Solution:</strong> Drop the GENERATED EXPRESSION manually after initial migration</p><p><strong>Problem:</strong> Prisma doesn’t update tsvector on model update<br> <strong>Solution:</strong> Use a PostgreSQL Trigger to keep tsvector in sync automatically</p><p><strong>Problem:</strong> Prisma cannot query tsvector fields<br> <strong>Solution:</strong> Use $queryRaw with JOINs and tsquery functions for FTS queries</p><p><strong>Problem:</strong> Prisma removes GIN indexes during drift checks<br> <strong>Solution:</strong> Define the GIN index initially, and Prisma won’t interfere after DROP EXPRESSION</p><h3>🎉 Benefits</h3><ul><li>100% Prisma compatible (no migration drift)</li><li>Full PostgreSQL Full-Text Search power</li><li>Prisma schema stays clean &amp; synced</li><li>Maintainable across migrations</li><li>Blazing fast with GIN index</li></ul><h3>📈 Bonus Tip:</h3><p>You can apply this same method to:</p><ul><li>JSON fields</li><li>Multiple columns combined into one tsvector</li><li>Weighting fields (e.g., title gets more importance than tags)</li></ul><h3>Conclusion</h3><p>Until Prisma officially supports tsvector full-text search natively, this approach is <strong>the most stable, production-ready solution</strong> for leveraging PostgreSQL FTS without Prisma migration headaches.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c421f63aaab3" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Make Your Resume ATS-Friendly with MyLiveCV]]></title>
            <link>https://medium.com/@chauhananubhav16/how-to-make-your-resume-ats-friendly-with-mylivecv-cfaa57866b66?source=rss-84db070f5373------2</link>
            <guid isPermaLink="false">https://medium.com/p/cfaa57866b66</guid>
            <category><![CDATA[writing]]></category>
            <category><![CDATA[business]]></category>
            <category><![CDATA[startup]]></category>
            <category><![CDATA[education]]></category>
            <dc:creator><![CDATA[Anubhav singh]]></dc:creator>
            <pubDate>Fri, 28 Feb 2025 14:36:57 GMT</pubDate>
            <atom:updated>2025-02-28T14:36:57.563Z</atom:updated>
            <content:encoded><![CDATA[<p>How to Make Your Resume ATS-Friendly with MyLiveCV</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Z0Cv7Ut5B3rRxYQX" /><figcaption>Photo by Marten Bjork on Unsplash</figcaption></figure><p>In today’s competitive job market, getting your resume past an Applicant Tracking System (ATS) is crucial. Many companies use ATS software to filter out resumes before they even reach human recruiters. If your resume isn’t optimized for ATS, it might never be seen—no matter how qualified you are.</p><p>At MyLiveCV, we help job seekers create ATS-friendly resumes that not only pass through these systems but also stand out to hiring managers. In this blog, we’ll explain what ATS is, why it matters, and how MyLiveCV can help you craft the perfect resume.</p><p>What is an ATS and Why Does It Matter?<br>An Applicant Tracking System (ATS) is software used by employers to streamline the hiring process. It scans, filters, and ranks resumes based on keywords, formatting, and structure before they reach a recruiter.</p><p>Common ATS issues job seekers face:<br>✔️ Wrong File Formats – Many ATS systems struggle with PDFs and images.<br>✔️ Overuse of Graphics – Fancy designs and tables may not be parsed correctly.<br>✔️ Lack of Keywords – If your resume doesn’t contain job-specific keywords, it might get rejected.<br>✔️ Unstructured Formatting – Irregular spacing, headers, and bullet points can confuse the system.</p><p>How to Make Your Resume ATS-Friendly<br>To increase your chances of getting noticed, follow these best practices:</p><p>✅ Use Standard File Formats – Submitting your resume in .docx or ATS-friendly PDFs is ideal.<br>✅ Keep It Simple – Avoid complex layouts, columns, and tables. Stick to clear headings and bullet points.<br>✅ Incorporate Keywords – Use job description keywords naturally throughout your resume.<br>✅ Choose the Right Font – Use Arial, Calibri, or Times New Roman in size 10-12 for better readability.<br>✅ Avoid Images and Icons – ATS software cannot read images, so don’t use logos or decorative elements.</p><p>How MyLiveCV Helps You Create the Perfect ATS-Friendly Resume<br>At MyLiveCV, we make it easy to create a resume that works with both ATS and human recruiters. Our platform provides:</p><p>✅ AI-Powered Resume Generation<br>Simply enter your details, and our AI automatically formats and optimizes your resume to pass ATS checks.</p><p>✅ Real-Time ATS Score &amp; Optimization Suggestions<br>Get instant feedback on what’s missing, including keyword recommendations, formatting improvements, and readability scores.</p><p>✅ One-Click Resume Improvement<br>Already have a resume? Upload it to instantly enhance its structure and content for ATS compatibility.</p><p>✅ Job-Specific Resume Customization<br>Generate tailored resumes for each job by adjusting keywords, experience, and skills based on the job description.</p><p>✅ SEO-Friendly Public Resume for Maximum Visibility<br>With MyLiveCV, you can create a public resume profile that ranks on search engines, helping recruiters find you online.</p><p>Final Thoughts<br>Your resume is your first impression, and if it doesn&#39;t pass ATS filters, you might miss out on great job opportunities. With MyLiveCV, you can ensure your resume is optimized for both technology and human recruiters, giving you the best chance to land your dream job.</p><p>👉 Start creating your ATS-friendly resume today at MyLiveCV.com!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cfaa57866b66" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[3 Simple Steps to Convert Your Resume into a Polished LinkedIn Profile with MyLiveCV]]></title>
            <link>https://medium.com/@chauhananubhav16/3-simple-steps-to-convert-your-resume-into-a-polished-linkedin-profile-with-mylivecv-c3a74c7485f0?source=rss-84db070f5373------2</link>
            <guid isPermaLink="false">https://medium.com/p/c3a74c7485f0</guid>
            <category><![CDATA[linkedin-resume-writer]]></category>
            <category><![CDATA[resume]]></category>
            <category><![CDATA[linkedin]]></category>
            <dc:creator><![CDATA[Anubhav singh]]></dc:creator>
            <pubDate>Sat, 22 Feb 2025 07:15:36 GMT</pubDate>
            <atom:updated>2025-02-22T07:15:36.140Z</atom:updated>
            <content:encoded><![CDATA[<p>Transform your resume into a standout LinkedIn profile in minutes! Discover how MyLiveCV’s Chrome extension streamlines your professional branding with 3 easy steps.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cEaZRRCEbTV0CTM1bnUH9A.jpeg" /></figure><blockquote><strong>LinkedIn profile optimization, resume to LinkedIn converter, MyLiveCV, Chrome extension for LinkedIn, professional resume builder</strong></blockquote><p>In today’s competitive job market, your LinkedIn profile isn’t just a digital resume — it’s your personal brand. Yet, keeping your resume and LinkedIn profile aligned can feel like a never-ending task. Enter <strong>MyLiveCV</strong>, a game-changing tool that bridges the gap between your resume and LinkedIn. Whether you’re job hunting, networking, or building your professional presence, here’s how to sync your credentials seamlessly in just three steps.</p><h3>Why Consistency Between Your Resume and LinkedIn Matters</h3><p>Recruiters and hiring managers <em>always</em> cross-reference your LinkedIn profile with your resume. Inconsistencies — like mismatched job dates or skills — can raise red flags. With MyLiveCV, you ensure both platforms reflect the same polished, professional story, saving time and boosting credibility.</p><h3>How to Import Your LinkedIn Profile to MyLiveCV in 3 Steps</h3><h4>Step 1: Log in to MyLiveCV</h4><p>Start by visiting the <a href="https://mylivecv.com/">MyLiveCV platform</a> (or create a free account if you’re new). This hub lets you build, edit, and store resumes while syncing them to LinkedIn.</p><p><strong>Pro Tip:</strong> MyLiveCV offers ATS-friendly templates, ensuring your resume passes through applicant tracking systems <em>and</em> impresses human readers.</p><h4>Step 2: Open LinkedIn and Navigate to Your Profile</h4><p>Log in to your LinkedIn account and go to your profile page. Keep this tab open — you’ll need it for the next step.</p><p><strong>Why This Matters:</strong> MyLiveCV’s Chrome extension pulls data directly from your LinkedIn profile, ensuring accuracy in titles, descriptions, and skills.</p><h4>Step 3: Use the MyLiveCV Chrome Extension</h4><ol><li>Install the <a href="https://chromewebstore.google.com/detail/mylivecv/ofhboldkkblldofknfcfhpficjgnjdid"><strong>MyLiveCV Chrome Extension</strong></a> (it’s free!).</li><li>Click the extension icon while on your LinkedIn profile.</li><li>Watch as MyLiveCV auto-populates your resume template with LinkedIn data.</li></ol><p><strong>Done!</strong> Edit formatting, add personal touches, and export your resume as a PDF — or use MyLiveCV to update your LinkedIn profile with new resume content.</p><h3>Why Professionals Love MyLiveCV</h3><ul><li><strong>Time-Saving:</strong> Ditch manual copy-pasting. Sync updates in seconds.</li><li><strong>Consistency:</strong> Eliminate discrepancies between your resume and LinkedIn.</li><li><strong>Professional Edge:</strong> Leverage sleek templates and SEO-friendly keywords to rank higher in recruiter searches.</li><li><strong>Centralized Control:</strong> Update your resume <em>or</em> LinkedIn through MyLiveCV’s platform — changes sync both ways.</li></ul><h3>Ready to Elevate Your Professional Brand?</h3><p>Your LinkedIn profile is your digital handshake. With MyLiveCV, you ensure it’s confident, consistent, and compelling.</p><p><strong>Get Started Now:</strong></p><ol><li>Install the <a href="https://chromewebstore.google.com/detail/mylivecv/ofhboldkkblldofknfcfhpficjgnjdid">MyLiveCV Chrome Extension</a>.</li><li>Visit <a href="https://mylivecv.com/">MyLiveCV.com</a> to refine your resume.</li></ol><p>👉 <strong>Pro Tip:</strong> Share your newly polished LinkedIn profile with your network! Tag @MyLiveCV and let us celebrate your upgrade.</p><p>#JobSearch #CareerGrowth #PersonalBranding #LinkedInTips #ResumeWriting</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c3a74c7485f0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Best Open-Source Alternatives to Strapi for API Management]]></title>
            <link>https://medium.com/@chauhananubhav16/best-open-source-alternatives-to-strapi-for-api-management-622dd6785469?source=rss-84db070f5373------2</link>
            <guid isPermaLink="false">https://medium.com/p/622dd6785469</guid>
            <category><![CDATA[cms]]></category>
            <category><![CDATA[payload]]></category>
            <category><![CDATA[strapi]]></category>
            <dc:creator><![CDATA[Anubhav singh]]></dc:creator>
            <pubDate>Fri, 14 Feb 2025 05:24:19 GMT</pubDate>
            <atom:updated>2025-02-14T05:24:19.456Z</atom:updated>
            <content:encoded><![CDATA[<h3>Best Open-Source Alternatives to Strapi for API Management</h3><p>Strapi has become a popular choice for managing content and APIs, offering a powerful editor, internationalization (i18n), and a user-friendly admin panel. However, if you’re looking for an alternative that focuses purely on API management while retaining an intuitive interface and i18n support, there are several open-source solutions worth considering.</p><p>In this blog, we’ll explore some of the best alternatives to Strapi that provide API management features, a robust editor, and internationalization support.</p><h3>1. Directus — API Management with a Lightweight CMS</h3><p><strong>Why Choose Directus?</strong></p><ul><li>Fully open-source and self-hosted</li><li>Built on top of SQL databases</li><li>Provides a headless CMS with a powerful admin panel</li><li>Offers REST &amp; GraphQL APIs</li><li>Role-based access control and permissions</li><li>Excellent support for internationalization (i18n)</li></ul><p>Directus is a great alternative for those who want an API-first approach with a flexible and lightweight admin interface.</p><p>🔗 <strong>GitHub:</strong> <a href="https://github.com/directus/directus">https://github.com/directus/directus</a></p><h3>2. KeystoneJS — A GraphQL-First CMS</h3><p><strong>Why Choose KeystoneJS?</strong></p><ul><li>GraphQL API out-of-the-box</li><li>Highly customizable with TypeScript support</li><li>Internationalization support</li><li>Fine-grained access control</li><li>Built-in authentication and user management</li></ul><p>KeystoneJS is an ideal choice if you need a GraphQL-based API with a flexible schema and robust access control.</p><p>🔗 <strong>GitHub:</strong> <a href="https://github.com/keystonejs/keystone">https://github.com/keystonejs/keystone</a></p><h3>3. Payload CMS — Secure and Scalable API Management</h3><p><strong>Why Choose Payload CMS?</strong></p><ul><li>Headless CMS with API management</li><li>Supports both REST and GraphQL APIs</li><li>Full TypeScript support</li><li>Advanced role-based access control</li><li>Integrated media management</li><li>i18n support</li></ul><p>Payload CMS is an excellent option if you need a powerful API-driven CMS with strong authentication and access control.</p><p>🔗 <strong>GitHub:</strong> <a href="https://github.com/payloadcms/payload">https://github.com/payloadcms/payload</a></p><h3>4. TinaCMS — Git-Based API and Content Management</h3><p><strong>Why Choose TinaCMS?</strong></p><ul><li>Git-powered CMS for structured content</li><li>Markdown and JSON-based backend</li><li>API-driven with i18n support</li><li>Supports modern front-end frameworks like Next.js and Gatsby</li></ul><p>TinaCMS is perfect for those who prefer a Git-based approach to content and API management, making it great for static site generators and JAMstack applications.</p><p>🔗 <strong>GitHub:</strong> <a href="https://github.com/tinacms/tinacms">https://github.com/tinacms/tinacms</a></p><h3>Which One Should You Choose?</h3><p>If you’re looking for a <strong>feature-rich CMS with a strong API</strong>, Directus or Payload CMS are the best options.</p><p>If you need a <strong>GraphQL-first approach</strong>, KeystoneJS is a solid choice.</p><p>For a <strong>Git-based alternative</strong>, TinaCMS is the way to go.</p><p>Each of these platforms provides a powerful way to manage APIs while offering internationalization and an intuitive admin interface. Depending on your use case, one of these Strapi alternatives might be the perfect fit for your project!</p><p>Which one are you planning to try? Let us know in the comments! 🚀</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=622dd6785469" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Biggest ATS Resume Mistakes That Could Cost You the Job]]></title>
            <link>https://medium.com/@chauhananubhav16/the-biggest-ats-resume-mistakes-that-could-cost-you-the-job-164209dd2b1b?source=rss-84db070f5373------2</link>
            <guid isPermaLink="false">https://medium.com/p/164209dd2b1b</guid>
            <category><![CDATA[resume]]></category>
            <category><![CDATA[ats-resume]]></category>
            <category><![CDATA[onlineresume]]></category>
            <category><![CDATA[free-resume]]></category>
            <category><![CDATA[cv]]></category>
            <dc:creator><![CDATA[Anubhav singh]]></dc:creator>
            <pubDate>Fri, 07 Feb 2025 18:16:00 GMT</pubDate>
            <atom:updated>2025-02-07T18:19:30.400Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CEi0W9Ot1eLiv4uEPVD4Lw.png" /></figure><p>Original Blog: <a href="https://mylivecv.com/resume/blogs/ats-mistakes">https://mylivecv.com/resume/blogs/ats-mistakes</a></p><p>To create an ATS-friendly resume with ResumeMCV Pro, visit <a href="https://mylivecv.com">MyLiveCV</a>!</p><h3>Introduction</h3><p>Applicant Tracking Systems (ATS) are increasingly being used by companies to filter resumes before they ever reach human recruiters. While ATS helps employers sift through a large pool of applicants, it can also cause qualified candidates to be overlooked if their resumes aren’t ATS-friendly. This guide highlights the biggest ATS resume mistakes and how you can avoid them to increase your chances of landing the job.</p><h3>1. Forgetting to Optimize for Keywords</h3><p>One of the biggest mistakes job seekers make is not tailoring their resumes with the right keywords. ATS scans resumes for specific keywords related to the job description, so using industry-specific terms is crucial for passing through the system.</p><h3>2. Using Fancy Fonts and Graphics</h3><p>ATS systems have difficulty reading non-standard fonts and images. Avoid using decorative fonts or adding logos, photos, and graphics, as these elements can cause the ATS to misinterpret or entirely skip over important information.</p><h3>3. Overcomplicating Your Resume Formatting</h3><p>While a visually appealing resume might impress a human recruiter, ATS can struggle with complex layouts. Stick to a simple, clean format with clear section headings such as “Experience,” “Education,” and “Skills.” Avoid using tables, text boxes, or multi-column layouts that may confuse the system.</p><h3>4. Using Uncommon File Types</h3><p>Always submit your resume in an ATS-friendly file format, such as .docx or .pdf. Avoid using file types that are not widely recognized, such as .txt or .rtf, as these can lead to formatting errors or be completely rejected by the system.</p><h3>5. Missing Essential Information</h3><p>ATS systems rely on certain resume sections to identify key information. Make sure your resume includes standard sections like “Contact Information,” “Work Experience,” and “Education” to ensure it’s parsed correctly.</p><h3>6. Not Including Relevant Skills</h3><p>ATS systems look for skills that match the job description. Make sure to list relevant skills clearly and use industry-standard terminology. This will ensure that your resume is not overlooked by the ATS and is flagged as a good match for the job.</p><h3>Conclusion</h3><p>To increase your chances of getting noticed by both ATS and hiring managers, avoid these common resume mistakes. By optimizing your resume for ATS, you can ensure that it passes through automated screenings and reaches the right hands. Regularly update and optimize your resume to stay ahead of the competition.</p><h3>References:</h3><p><a href="https://mylivecv.com/resume/blogs/live-resume-tips">https://mylivecv.com/resume/blogs/live-resume-tips</a><br><a href="https://mylivecv.com/resume/blogs/online-resume-visibility">https://mylivecv.com/resume/blogs/online-resume-visibility</a><br><a href="https://mylivecv.com/resume/blogs/resume-writing-tips-2024">https://mylivecv.com/resume/blogs/resume-writing-tips-2024</a><br><a href="https://mylivecv.com/resume/blogs/ats-keywords-resume">https://mylivecv.com/resume/blogs/ats-keywords-resume</a><br><a href="https://mylivecv.com/resume/blogs/resume-formatting-guide">https://mylivecv.com/resume/blogs/resume-formatting-guide</a><br><a href="https://mylivecv.com/resume/blogs/resume-action-words">https://mylivecv.com/resume/blogs/resume-action-words</a><br><a href="https://mylivecv.com/resume/blogs/transferable-skills-resume">https://mylivecv.com/resume/blogs/transferable-skills-resume</a><br><a href="https://mylivecv.com/cover-letter/blogs/live-cover-letter-benefits">https://mylivecv.com/cover-letter/blogs/live-cover-letter-benefits</a><br><a href="https://mylivecv.com/cover-letter/blogs/digital-cover-letter-tips">https://mylivecv.com/cover-letter/blogs/digital-cover-letter-tips</a><br><a href="https://mylivecv.com/cover-letter/blogs/write-cover-letter-2024">https://mylivecv.com/cover-letter/blogs/write-cover-letter-2024</a><br><a href="https://mylivecv.com/cover-letter/blogs/common-cover-letter-mistakes">https://mylivecv.com/cover-letter/blogs/common-cover-letter-mistakes</a><br><a href="https://mylivecv.com/cover-letter/blogs/personalization-cover-letter">https://mylivecv.com/cover-letter/blogs/personalization-cover-letter</a><br><a href="https://mylivecv.com/cover-letter/blogs/cover-letter-templates">https://mylivecv.com/cover-letter/blogs/cover-letter-templates</a><br><a href="https://mylivecv.com/cover-letter/blogs/address-gaps-career-changes">https://mylivecv.com/cover-letter/blogs/address-gaps-career-changes</a><br><a href="https://mylivecv.com/portfolio/blogs/professional-portfolio-tips">https://mylivecv.com/portfolio/blogs/professional-portfolio-tips</a><br><a href="https://mylivecv.com/portfolio/blogs/digital-portfolio-tools">https://mylivecv.com/portfolio/blogs/digital-portfolio-tools</a><br><a href="https://mylivecv.com/portfolio/blogs/creative-portfolio-checklist">https://mylivecv.com/portfolio/blogs/creative-portfolio-checklist</a><br><a href="https://mylivecv.com/portfolio/blogs/tailor-portfolio-job-applications">https://mylivecv.com/portfolio/blogs/tailor-portfolio-job-applications</a><br><a href="https://mylivecv.com/portfolio/blogs/portfolio-design-tips">https://mylivecv.com/portfolio/blogs/portfolio-design-tips</a><br><a href="https://mylivecv.com/portfolio/blogs/live-portfolio-benefits">https://mylivecv.com/portfolio/blogs/live-portfolio-benefits</a><br><a href="https://mylivecv.com/portfolio/blogs/digital-portfolio-visibility">https://mylivecv.com/portfolio/blogs/digital-portfolio-visibility</a><br><a href="https://mylivecv.com/resume/blogs/ats-friendly-resume">https://mylivecv.com/resume/blogs/ats-friendly-resume</a><br><a href="https://mylivecv.com/resume/blogs/ats-resume-checklist">https://mylivecv.com/resume/blogs/ats-resume-checklist</a><br><a href="https://mylivecv.com/resume/blogs/ats-resume-formatting">https://mylivecv.com/resume/blogs/ats-resume-formatting</a><br><a href="https://mylivecv.com/resume/blogs/ats-keywords">https://mylivecv.com/resume/blogs/ats-keywords</a><br><a href="https://mylivecv.com/resume/blogs/ats-test">https://mylivecv.com/resume/blogs/ats-test</a><br><a href="https://mylivecv.com/resume/blogs/ats-proof-resume-tips">https://mylivecv.com/resume/blogs/ats-proof-resume-tips</a><br><a href="https://mylivecv.com/resume/blogs/ats-mistakes">https://mylivecv.com/resume/blogs/ats-mistakes</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=164209dd2b1b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How MyliveCV Helps You Beat the ATS and Land More Interviews]]></title>
            <link>https://medium.com/@chauhananubhav16/how-mylivecv-helps-you-beat-the-ats-and-land-more-interviews-fd0c8faa64d0?source=rss-84db070f5373------2</link>
            <guid isPermaLink="false">https://medium.com/p/fd0c8faa64d0</guid>
            <category><![CDATA[resume]]></category>
            <category><![CDATA[ats-resume-score]]></category>
            <category><![CDATA[at]]></category>
            <category><![CDATA[free-resume]]></category>
            <category><![CDATA[cheap-resume]]></category>
            <dc:creator><![CDATA[Anubhav singh]]></dc:creator>
            <pubDate>Wed, 05 Feb 2025 17:50:00 GMT</pubDate>
            <atom:updated>2025-02-05T17:50:00.516Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HP-9IqAfQO8A9zSno_va0A.jpeg" /></figure><p>Are you applying for jobs but not getting responses? Your resume might be getting rejected by <strong>Applicant Tracking Systems (ATS)</strong> before it even reaches a hiring manager. <a href="https://www.mylivecv.com/"><strong>MyliveCV.com</strong></a> is here to help you create an ATS-friendly resume that gets noticed and lands you interviews!</p><h3>🚀 What is an ATS, and Why Does It Matter?</h3><p>An <strong>Applicant Tracking System (ATS)</strong> is software used by recruiters to <strong>filter and rank resumes</strong> before they ever see them. It scans resumes for <strong>keywords, formatting, and structure</strong>, often rejecting those that don’t meet specific criteria.</p><p>If your resume isn’t <strong>ATS-optimized</strong>, it might never reach a human recruiter — even if you’re the perfect candidate for the job!</p><h3>✅ How MyliveCV Ensures Your Resume is ATS-Friendly</h3><p>MyliveCV is designed to create resumes that pass <strong>ATS screenings</strong> while still looking professional and polished. Here’s how:</p><h3>1️⃣ ATS-Compatible Formatting</h3><ul><li>Uses <strong>clean layouts</strong> that are easy for ATS to scan.</li><li>Avoids <strong>tables, images, and columns</strong> that ATS may struggle to read.</li><li>Maintains proper <strong>font styles and sizes</strong> to ensure readability.</li></ul><h3>2️⃣ Keyword Optimization</h3><ul><li>AI-powered suggestions help you include <strong>industry-specific keywords</strong>.</li><li>Helps match your resume content with the <strong>job description</strong> for better ranking.</li><li>Provides <strong>action-driven bullet points</strong> to showcase your skills effectively.</li></ul><h3>3️⃣ Proper Resume Structure</h3><ul><li>Automatically formats sections like <strong>Work Experience, Skills, and Education</strong> in an ATS-friendly way.</li><li>Ensures <strong>consistent headings and bullet points</strong> for better parsing.</li><li>Avoids unnecessary graphics or decorative elements that may confuse the ATS.</li></ul><h3>4️⃣ PDF &amp; Text Export for Maximum Compatibility</h3><ul><li>Download resumes in <strong>ATS-friendly PDF or plain text formats</strong>.</li><li>Ensures your file doesn’t get rejected due to incompatible formatting.</li><li>Provides <strong>editable templates</strong> so you can tailor your resume for each job.</li></ul><h3>🎯 Who Needs an ATS-Friendly Resume?</h3><p>✅ <strong>Job Seekers:</strong> Avoid resume rejections and boost interview chances. ✅ <strong>Career Changers:</strong> Optimize for new roles by tailoring keywords and skills. ✅ <strong>Entry-Level Applicants:</strong> Ensure your resume passes ATS filters even without much experience. ✅ <strong>Freelancers &amp; Remote Workers:</strong> Get noticed for roles in competitive job markets.</p><h3>🌟 Get More Interviews with MyliveCV!</h3><p>Don’t let an ATS be why you miss out on job opportunities. <strong>MyliveCV ensures your resume is formatted, optimized, and ATS-ready</strong> so you can focus on landing your dream job.</p><p>👉 <strong>Start building your ATS-friendly resume today at </strong><a href="https://www.mylivecv.com/"><strong>MyliveCV.com</strong></a><strong>!</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fd0c8faa64d0" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Create Stunning Resumes, Portfolios, and Cover Letters with MyliveCV]]></title>
            <link>https://medium.com/@chauhananubhav16/create-stunning-resumes-portfolios-and-cover-letters-with-mylivecv-4ee69be14e44?source=rss-84db070f5373------2</link>
            <guid isPermaLink="false">https://medium.com/p/4ee69be14e44</guid>
            <category><![CDATA[cv]]></category>
            <category><![CDATA[free-resume]]></category>
            <category><![CDATA[resume-builder]]></category>
            <category><![CDATA[resume]]></category>
            <dc:creator><![CDATA[Anubhav singh]]></dc:creator>
            <pubDate>Wed, 05 Feb 2025 17:47:21 GMT</pubDate>
            <atom:updated>2025-02-05T17:48:36.356Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HP-9IqAfQO8A9zSno_va0A.jpeg" /></figure><p>Are you tired of spending hours formatting your resume, struggling with portfolio design, or crafting the perfect cover letter? Look no further! <a href="https://www.mylivecv.com/"><strong>MyliveCV.com</strong></a> is your all-in-one platform <strong>for effortlessly creating, customizing, and sharing professional resumes, portfolios, and cover letters</strong>. Whether you’re a job seeker, freelancer, or entrepreneur, MyliveCV is designed to make you stand out in the competitive job market.</p><h3>🚀 Why Choose MyliveCV?</h3><p>MyliveCV goes beyond just resume creation — it’s a <strong>complete career-building tool</strong> that helps you showcase your skills and achievements like never before. Here’s what makes it special:</p><h3>1️⃣ Create Professional Resumes in Minutes</h3><ul><li>Choose from a variety of expertly designed <strong>resume templates</strong> that highlight your skills.</li><li><strong>Import your LinkedIn profile</strong> instantly to populate your resume.</li><li>Use AI-powered suggestions to optimize content and ensure a <strong>standout resume</strong>.</li><li>Export your resume in <strong>PDF format</strong> or share it via a public link.</li></ul><h3>2️⃣ Build a Stunning Portfolio</h3><ul><li>Showcase your <strong>projects, work samples, and achievements</strong> in a beautifully designed portfolio.</li><li>Customize your portfolio layout to match your brand.</li><li>Get a <strong>custom shareable link</strong> to send your portfolio to employers or clients.</li></ul><h3>3️⃣ Generate Cover Letters with Ease</h3><ul><li><strong>AI-assisted cover letter generation</strong> tailored to your resume and job application.</li><li>Professionally formatted and customizable templates.</li><li>Save and reuse cover letters for multiple applications.</li></ul><h3>4️⃣ Seamless LinkedIn Integration</h3><ul><li><strong>Import your LinkedIn profile</strong> in one click to auto-fill your resume details.</li><li>Save time by avoiding manual data entry.</li></ul><h3>5️⃣ Multiple Templates &amp; Customization</h3><ul><li>Select from <strong>modern, creative, or traditional</strong> resume templates.</li><li>Customize colors, fonts, and layouts to match your <strong>style</strong>.</li><li>Preview your resume, cover letter, and portfolio in <strong>real time</strong>.</li></ul><h3>6️⃣ Easy Sharing &amp; Public Profiles</h3><ul><li>Get a <strong>unique URL</strong> to share your resume, portfolio, or cover letter online.</li><li><strong>Boost your online presence</strong> with a professional-looking public profile.</li><li>Control who sees your documents with privacy settings.</li></ul><h3>7️⃣ PDF Export &amp; Download</h3><ul><li>Export your resume, portfolio, or cover letter in <strong>high-quality PDF format</strong>.</li><li>Ensure compatibility with <strong>applicant tracking systems (ATS)</strong>.</li></ul><h3>8️⃣ Optimized for SEO &amp; Job Search</h3><ul><li>Make your public profile discoverable by recruiters through <strong>SEO optimization</strong>.</li><li>Improve your chances of landing a job with a <strong>well-structured online presence</strong>.</li></ul><h3>🎯 Who Can Benefit from MyliveCV?</h3><p>✅ <strong>Job Seekers:</strong> Impress recruiters with a professionally designed resume and cover letter. ✅ <strong>Freelancers &amp; Creatives:</strong> Showcase your work in a beautiful portfolio to attract clients. ✅ <strong>Students &amp; Fresh Graduates:</strong> Create your first professional resume with ease. ✅ <strong>Entrepreneurs &amp; Consultants:</strong> Share your career highlights and achievements with potential partners.</p><h3>🌟 Get Started Today!</h3><p>Don’t let outdated resumes and portfolios hold you back. <strong>Join thousands of professionals using MyliveCV</strong> to create a lasting impression and advance their careers.</p><p>👉 <strong>Visit </strong><a href="https://www.mylivecv.com/"><strong>MyliveCV.com</strong></a><strong> now and build your perfect resume, portfolio, and cover letter in minutes!</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4ee69be14e44" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>