Category: Open-source

  • Code as Context for l10n

    Why WordPress Demands “مخطّط المستند” Over Legacy Translations

    In software localization, a string in isolation is a trap. When translating a massive platform like WordPress, looking at a spreadsheet of isolated English phrases often leads to “safe” but clunky translations. The ultimate source of truth is never the glossary—it is the source code.

    For years, the Arabic localization for “Document Outline” has been ملخص عناوين المستند (literally: Summary of document headings). Born from an era where translators felt the need to heavily explain UI features to users (a process called explicitation), this string is wordy, descriptive, and ultimately outdated.

    As we look toward modern WordPress UI paradigms, it is time to standardize this string to مخطّط المستند. To understand exactly why this update is necessary, we don’t need to debate linguistics—we just need to look at the code.

    The Reference Code: A Window into the UI

    Let’s examine the exact file where this string lives in the Gutenberg block editor: wp-includes/js/dist/editor.js, specifically within the TableOfContentsPanel component.

    Here is the structural blueprint of that panel:

    JavaScript

    // ... [Lines 49560 - 49617 omitted for brevity] ...
    <div className="table-of-contents__wrapper" role="note" aria-label={__("Document Statistics")}>
        {/* Renders Counts: Words, Characters, Time to read, Headings, Paragraphs, Blocks */}
    </div>
    
    { headingCount > 0 && (
        <>
            <hr />
            <h2 className="table-of-contents__title">{__("Document Outline")}</h2>
            <DocumentOutline
                onSelect={onRequestClose}
                hasOutlineItemsDisabled={hasOutlineItemsDisabled}
            />
        </>
    )}
    // ... [Line 49630] ...
    

    By reading this snippet, the context transforms the way we must approach the translation. Here is how the reference code guides us to a vastly superior Arabic localization.

    1. The Code Proves It’s a Structure, Not a “Summary”

    Look at the logic in the code. The panel is divided into two distinct sections. The top half is strictly for Document Statistics (Words, Characters, reading time). This top section acts as the actual summary of the document’s contents.

    Below the <hr /> (horizontal rule), we see the Document Outline. If we keep the legacy translation ملخص عناوين المستند (Summary of headings), we create a semantic clash. We are placing a “summary” directly underneath actual statistical summaries.

    The <DocumentOutline/> component rendered here is a navigational tree—a structural skeleton that lets users jump between header blocks. مخطّط means “plan,” “map,” or “outline.” By using مخطّط المستند, we accurately tell the user they are looking at a structural map, differentiating it cleanly from the statistics above it.

    2. The Original File Path Reveals Architectural Intent While the bundled distribution code lives in editor.js, line 49560 provides a crucial breadcrumb to the original, uncompiled source code: // packages/editor/build-module/components/table-of-contents/panel.mjs.

    This file path is a L10n goldmine. It explicitly identifies that this UI element belongs to the table-of-contents component. A Table of Contents is inherently a navigational map of a document’s hierarchy; it is never a summary of the text itself. By organizing the code under table-of-contents, the core engineering team is signaling clear structural intent.

    Translating this feature as a ملخص (summary) directly contradicts the architectural blueprint of the software. Conversely, مخطّط (outline/map) perfectly echoes the navigational purpose of a Table of Contents, ensuring the Arabic string aligns with the foundational logic of the Gutenberg editor.

    3. The <h2> Tag Demands Brevity

    Notice the HTML element wrapping our string:

    <h2 className="table-of-contents__title">{__("Document Outline")}</h2>

    This string is not explanatory text; it is a section heading (<h2>). In UI design, headings must be punchy, scannable, and instantly recognizable.

    • Legacy: ملخص عناوين المستند (3 words, visually heavy)
    • Proposed: مخطّط المستند (2 words, crisp and standard)

    Users do not read software interfaces; they scan them. A three-word, overly descriptive heading slows down the user’s cognitive parsing. مخطّط المستند functions perfectly as a quick, scannable title.

    4. Spatial Constraints of the Sidebar Panel

    The component name is TableOfContentsPanel. In the WordPress editor, this panel is a narrow sidebar or a popover menu. UI real estate is highly restricted.

    When a multi-word string like ملخص عناوين المستند is crammed into a narrow sidebar, it risks truncation (e.g., ملخص عناوين ا...) or awkward line breaks, especially on mobile views. The source code reveals the physical constraints of the UI, proving that the shorter, more compact مخطّط المستند is a functional necessity to preserve the design layout.

    5. Aligning with Industry Standards

    As localization specialists, consistency across the digital ecosystem is one of our primary goals. When users switch between Microsoft Word, Google Docs, Apple Pages, and various web-based CMS platforms, they shouldn’t have to learn a new vocabulary for the exact same tool.

    Major tech giants have already recognized the need for concise, structural terminology:

    • Google Docs currently utilizes مخطّط المستند for its outline feature.
    • Microsoft Word uses similar structural terminology, like جزء التنقل (Navigation Pane) or مخطط المستند (Document Map in older versions).

    By updating to مخطّط المستند, we align our product with the established mental models of millions of Arab users. We stop forcing them to translate our unique legacy jargon and instead speak the language they already know.

    Conclusion: Translating for the Future

    WordPress is a constantly evolving ecosystem. With modern iterations of the block editor, the UI is becoming cleaner, faster, and more minimalist. Our Arabic localization must evolve with it.

    Relying on the source code changes a translator from a mere linguist into a UI/UX advocate. The code at editor.js:49618 explicitly tells us that “Document Outline” is a compact, structural heading housed within a narrow panel. By dropping the outdated, hand-holding explicitation, observing UI boundaries, and adopting the industry standard مخطّط المستند, we respect the developer’s design and ultimately provide a more native, professional experience for the Arab WordPress user.

  • Decoding WordPress PHP: Why Do We Use if ( … ) : Instead of {}?

    As I dive deeper into the world of WordPress Full Site Editing (FSE) theme development, I’m spending a lot of time dissecting the default Block Themes—like Twenty Twenty-Five.

    FSE is shifting so much of theme development into theme.json and block markup, but PHP remains the backbone of how our themes actually function. Recently, while looking at the functions.php file in a default theme, I stumbled upon a piece of syntax that always used to confuse me:

    if ( ! function_exists( 'my_theme_setup' ) ) :
        // ... function code here ...
    endif;

    Wait a second. Where are the curly braces {}? Why is there a colon : after the if statement?

    If you’ve ever asked yourself the same questions, you aren’t alone. Today, I want to break down this quirky PHP syntax, explain why WordPress loves it, and discuss whether you should be using it in your own FSE themes.

    The Alternative Syntax Explained

    That colon is part of PHP’s alternative syntax for control structures. It does the exact same thing as the standard curly-brace syntax; it’s just a different way of writing it.

    Here is a side-by-side comparison:

    The Standard Way:

    if ( ! function_exists( 'my_theme_setup' ) ) {
        function my_theme_setup() {
            // Setup code
        }
    }

    The Alternative Way:

    if ( ! function_exists( 'my_theme_setup' ) ) :
        function my_theme_setup() {
            // Setup code
        }
    endif;

    Functionally, these are identical. PHP doesn’t care which one you use. But if they do the same thing, why does WordPress consistently use the colon method?

    The “Why”: Readability in the Trenches

    The primary reason for the alternative syntax is readability when mixing PHP and HTML.

    As theme developers, we are constantly jumping in and out of PHP to output HTML. When you have nested if statements, foreach loops, and while loops all mixed with HTML, a wall of closing curly braces } becomes a nightmare to read. You find yourself scrolling up and down asking, “Which } closes the if statement, and which closes the loop?”

    The alternative syntax solves this by explicitly labeling the closing tag. Consider this classic loop example:

    Using Curly Braces (Hard to read):

    <?php if ( have_posts() ) { ?>
        <div class="post-list">
            <?php while ( have_posts() ) { 
                the_post(); ?>
                <h2><?php the_title(); ?></h2>
            <?php } ?>
        </div>
    <?php } ?>

    Look at the bottom. You have } and }. It’s not immediately obvious what belongs to what.

    Using the Colon Syntax (Much clearer):

    <?php if ( have_posts() ) : ?>
        <div class="post-list">
            <?php while ( have_posts() ) : 
                the_post(); ?>
                <h2><?php the_title(); ?></h2>
            <?php endwhile; ?>
        </div>
    <?php endif; ?>

    Now, the bottom is perfectly clear: endwhile; closes the loop, and endif; closes the condition. It’s a breath of fresh air for readability.

    Why use it in functions.php?

    You might be thinking: “Okay, I get it for HTML templates. But the code snippet I showed at the beginning was from functions.php, which is pure PHP! Why use it there?”

    You caught me! In pure PHP files, the readability benefit of mixing HTML doesn’t apply. However, in WordPress, using the colon syntax for pluggable functions has become a cultural convention.

    The if ( ! function_exists() ) : pattern is how WordPress creates “pluggable” functions. It allows a child theme to define the same function, effectively overriding the parent theme. Because this is such a vital concept in WordPress theme development, using endif; acts as a visual “bookend.” When you are scrolling through hundreds of lines of code in functions.php and see endif;, you instantly know: “Ah, this is the end of a pluggable function override check.”

    Is it Best Practice for FSE Theme Developers?

    This is where nuance comes in. The short answer is: It depends on the file.

    • In PHP Template Files / Block Render Callbacks: YES. Even in FSE, you will write PHP to render dynamic blocks or create custom block patterns via PHP. Whenever you are mixing PHP logic and HTML output, using if : ... endif; and while : ... endwhile; is a WordPress best practice. It is explicitly recommended in the WordPress Coding Standards.
    • In Pure Logic Files (functions.php, Classes): IT’S UP TO YOU. General PHP standards (like PSR-12) actually prefer curly braces {} for pure logic files. However, because WordPress has its own long-standing conventions, you will see the colon syntax used frequently even in functions.php. Neither is “wrong” here.

    The Takeaway

    As FSE theme developers, we are navigating the bridge between classic WordPress PHP conventions and modern block-based development. While theme.json handles most of the visual setup now, understanding why WordPress code looks the way it does empowers us to write cleaner, more maintainable PHP for our dynamic blocks and theme setups.

    The next time you see that colon : in a theme file, you don’t need to be confused. You can just smile, knowing it’s there to make your life a little easier when you’re deep in the code.


    Are you building FSE themes? What PHP quirks have you run into lately? Let’s chat in the comments below!

  • Mass Editing in l10n

    Mastering Technical Localization: A Case Study on Mass Editing “Purge” in Arabic

    In the world of software localization, consistency is not just a luxury; it is a necessity. When users interact with a technical interface, they rely on predictable terminology. If a button says “Purge” in one menu and “Clear” in another, but the underlying action is similar, confusion arises.

    This article explores a practical workflow for mass-editing and improving Arabic localization, using the term “Purge” (translated as “محو”) as a case study. We will look at how to leverage filtering tools and rapid .po file editing with simple text editors to ensure speed and accuracy.


    The Challenge: “Purge” in a Technical Context

    The word “Purge” in computing—specifically regarding caching—implies a forceful removal of data. It is stronger than “Clear” or “Delete.”

    In Arabic, translators often oscillate between:

    • مسح (Mas’h): Wiping/Clearing (Common for “Clear Cache”).
    • حذف (Hadhf): Deleting.
    • محو (Mahw): Erasing/Obliterating (A strong, precise term for “Purge”).

    As seen in the provided screenshot, the translation team has settled on “محو” (Mahw) for “Purge” and “محو ذاكرة التخزين المؤقت” for “Purge Cache.” This is a solid choice because it distinguishes the action from a simple “Clear.”

    Step 1: Leveraging the Filter and Search

    The screenshot shows a translation management interface, GlotPress. The most powerful feature here is the Search and Filter combination.

    1. The Search Bar: At the top, the user has searched for purge. The system highlights the term in yellow/orange within the “Original string” column. This immediately visualizes every instance where the term appears.
    2. The Filter: Notice the link “Current Filter (12)”. This indicates that the view is currently restricted to a specific subset of strings.
    3. The Result Count: The top bar shows 1/14. This means there are 14 total instances of “purge” in this web page.

    Why this matters: Instead of scrolling through 325 strings (as shown in “All (325)”), the specialist can focus entirely on the 14 relevant strings. This is the first step in mass editing.

    Step 2: Analyzing Context and Grammar

    Before mass editing, we must ensure the translation fits the grammatical context. Arabic is a highly inflected language, meaning words change form based on gender and number.

    Looking at the screenshot:

    • Imperative/Noun: “Purge SG Cache” is translated as محو ذاكرة التخزين المؤقت SG. Here, “Purge” is treated as a verbal noun (Masdar). This is correct for UI buttons.
    • Passive Verb: In the first long string: “…once it is purged…”. The translation is “…بمجرد محوها…” (once it [feminine] is erased).
      • Specialist Note: The translator correctly identified that “it” refers to “cache” (ذاكرة – feminine) or the implied object, and added the suffix “ها” (ha) to “محو”. This shows high attention to detail.

    Step 3: The Rapid Mass Edit Workflow (Text Editor + .po File)

    While the web interface is great for review, editing strings one by one is slow and inefficient. The fastest method for mass editing is exporting the .po file, editing it with a simple text editor, and re-importing it to GlotPress.

    Here is the recommended workflow:

    3.1 Export the .po File

    From GlotPress, navigate to your project and export the Arabic translation file (ar.po). This file contains all your translations in a simple, human-readable format.

    3.2 Open with a Text Editor

    You don’t need expensive CAT tools. Simple, powerful text editors work perfectly:

    • VS Code (Visual Studio Code) – Free, powerful search/replace
    • Text Editor by Gnome Project

    3.3 Perform Mass Find and Replace

    This is where the magic happens. Let’s say you want to ensure consistency for “Purge” → “محو”:

    Example Scenario:
    You notice that some translations use “مسح” while others use “محو” for “Purge.” You want to standardize everything to “محو”.

    In your text editor:

    1. Open Find/Replace (Ctrl+H or Cmd+H)
    2. Search for: msgstr "مسح ذاكرة التخزين المؤقت"
    3. Replace with: msgstr "محو ذاكرة التخزين المؤقت"
    4. Replace All

    Handling Context Variations:
    Looking at the screenshot, you might need to handle:

    • msgstr "محو ذاكرة التخزين المؤقت" (Purge Cache)
    • msgstr "محو يدوي لذاكرة التخزين المؤقت" (Manual Cache Purge)
    • msgstr "محو ذاكرة التخزين المؤقت SG" (Purge SG Cache)

    With a text editor, you can quickly review all instances and make bulk changes while maintaining the different grammatical forms.

    3.4 Validate the .po File

    Before importing back:

    • Most text editors will show syntax highlighting for .po files
    • Check that all msgid (English) and msgstr (Arabic) pairs are intact
    • Ensure no quotation marks are broken
    • Verify that Arabic text is properly encoded (UTF-8)

    3.5 Import Back to GlotPress

    1. Go to your GlotPress project
    2. Navigate to Import Translations
    3. Upload your edited ar.po file
    4. Choose the import options:
      • Import existing translations as suggestions (safer, requires review)
      • Overwrite existing translations (faster, but be careful!)
    5. Click Import

    Pro Tip: For large projects, import as “suggestions” first, then use GlotPress’s bulk approval feature to approve all changes at once after a quick review.

    Step 4: Handling Acronyms and Technical Terms

    The screenshot provides excellent examples of handling technical terms that should not be translated:

    • “SG Cache”: Translated as ذاكرة التخزين المؤقت SG. The acronym “SG” (likely SiteGround) is kept in English. This is correct.
    • “wp-cron”: Kept as wp-cron in the Arabic text. This is crucial because translating technical function names breaks the software logic.
    • “URLs”: Translated as رابط (links/URLs). This is a good localization choice, making it understandable for Arabic users while keeping the number “200”.

    When doing mass edits in a text editor, you can use negative lookaheads in regex to avoid replacing technical terms. For example, ensure you don’t accidentally replace “purge” inside code comments or function names.

    Benefits of the Text Editor Workflow

    1. Speed: What takes 30 minutes of clicking in GlotPress takes 2 minutes in a text editor
    2. Consistency: One find/replace ensures every instance is updated
    3. Flexibility: Use regex for complex patterns
    4. No Dependencies: No need for expensive CAT tool licenses
    5. Version Control: You can track changes in the .po file using Git
    6. Reusability: Save your find/replace patterns for future projects
    Editing a .po file using Gnome text editor

    Common Pitfalls to Avoid

    ⚠️ Always backup your .po file before mass editing!

    ⚠️ Don’t break the .po syntax:

    • Keep msgid and msgstr on separate lines
    • Maintain quotation marks
    • Don’t remove the msgid "" entries

    ⚠️ Watch for context:

    • “Purge” as a verb vs. noun may need different Arabic forms
    • Plural forms in Arabic are complex (singular, dual, plural)
    • Ensure gender agreement (محوها vs محوه)

    ⚠️ Test after import:

    • Always verify a few strings in GlotPress after importing
    • Check that special characters display correctly
    • Ensure no HTML entities are broken

    Conclusion

    Mass editing localization is not just about speed; it is about uniformity and efficiency. By using the filter function to isolate terms like “purge” (as seen in the screenshot), a specialist can review all instances quickly.

    However, the real time-saver is the export → text editor → import workflow. This method allows you to:

    • Make hundreds of changes in seconds
    • Maintain perfect consistency across your project
    • Work offline without platform limitations
    • Use powerful search patterns (regex) for complex edits

    For Arabic localization specifically, where grammatical variations are common, this workflow ensures that “Purge” is always “محو” and “Purged” is grammatically correct throughout the entire project.

    Key Takeaway: Filter to identify the scope, export the .po file, use a simple text editor for mass find/replace operations, and import back to GlotPress. This workflow transforms hours of manual work into minutes of efficient editing while maintaining the highest quality standards.

  • Localizing Colors in WordPress Default Themes to Arabic

    Balancing Aesthetics and UX:

    When localizing default WordPress theme like Twenty Twenty-Four, a translator’s biggest challenge isn’t finding literal dictionary equivalents—it’s balancing design aesthetics with dashboard usability. The translated terms must look polished in the interface and make immediate sense to the end-user building their site.


    The Anatomy of a Perfect Gradient Translation

    Take the English string “Vertical soft rust to white”. A literal approach might produce something clunky like ” صدئي عمودي ناعم إلى الأبيض” (rusty vertical soft to white) or “صدا عمودي ناعم إلى الأبيض” (rust vertical soft to white). However, the most natural, professional translation for a WordPress design UI is “تدرج عمودي ناعم من الصدئي إلى الأبيض”.

    This phrasing works perfectly for two reasons:

    • Functional Priority: Starting with “تدرج عمودي ناعم” instantly tells the user they are looking at a smooth, fluid gradient tool rather than a static color block.
    • Grammatical Elegance: Using the descriptive relative adjective “الصدئي” captures the warm, premium essence of the original “rust” palette while keeping the Arabic grammar fluid and intuitive for site builders.

    Why Visual Clarity Beats Literal Transliteration

    A similar strategic choice had to be made for the color “pewter.” While the temptation exists to simply transliterate it as “البيوتر”, doing so creates a poor user experience. Pewter is a specific tin alloy. To an average Arabic user adjusting their site editor blocks, “البيوتر” carries no visual meaning—it forces them to guess what the shade looks like.

    By localizing it as “الرصاصي”, the interface immediately communicates that the color is a cool, metallic shade of gray. This decision really proves its value in complex dual-color strings like “Vertical hard pewter to dark gray.” Translating it with “الرصاصي” yields “تدرج عمودي حاد من الرصاصي إلى الرمادي الداكن”. That preserves a clear visual contrast between two distinct grays, avoiding the linguistic redundancy of saying “من الرمادي إلى الرمادي الداكن” (from gray to dark gray).


    Conclusion

    Ultimately, successful localization is about preserving the designer’s intent while honoring the end-user’s language flow. Sticking to clear, visually descriptive terms ensures the WordPress dashboard remains a powerful, accessible tool for the global Arabic community.

  • The WP‑CLI Command That Saved Me Hours on URL Restructuring


    How I Restructured My Arabic Learning Site’s URLs Without Losing SEO (Using WP‑CLI & Redirection)

    A few weeks ago I faced a classic content‑site dilemma.
    My website, Lucid Arabic, had around 80 of beginner vocabulary lessons with clean but flat URLs like:

    https://lucidarabic.com/how-to-say-star-in-arabic

    I knew I wanted to expand into intermediate and advanced content, grammar lessons, and exercises. The flat structure just wouldn’t scale. I needed a hierarchy that would make sense for learners and search engines alike:

    https://lucidarabic.com/beginner/vocabulary/how-to-say-star-in-arabic

    The goal was clear, but the path was full of potential SEO landmines. After a lot of planning (and a successful test on a local site), I pulled it off with zero broken links and zero ranking drops. Here’s exactly how I did it, including the WP‑CLI command that saved me hours and the critical step most people overlook.


    The Challenge

    • Old URL pattern: /%postname%/ (e.g., /how-to-say-star-in-arabic/)
    • New URL pattern: /beginner/vocabulary/%postname%/
    • 80+ published posts that needed to move seamlessly
    • No room for 404 errors – existing traffic and search rankings had to be preserved

    I needed a method that would:

    1. Automatically assign the correct category to all existing posts
    2. Generate the new hierarchical URLs on the fly
    3. Create precise 301 redirects from every old URL to its new counterpart
    4. Update the XML sitemap so Google would see the new structure immediately

    WordPress makes steps 1 and 2 easy if you’ve cleaned up your categories first. Step 3 is where WP‑CLI turned a tedious task into a 30‑second job.


    Step 1: Create the New Category Hierarchy

    First, I set up the container categories in Posts → Categories:

    • beginner (slug: beginner)
    • vocabulary (slug: vocabulary) – with beginner as its parent

    This gives the path beginner/vocabulary. I made sure no other posts were using these slugs to avoid conflicts.


    Step 2: The Critical Clean‑up – Assign ONLY the Vocabulary Category

    “If you change the permalink structure while posts still have other categories assigned, those posts will immediately take on URLs that include those old category slugs (e.g., /uncategorized/something/, or /some-other-category/something/). That would create a mess before you even get to the redirect step.”

    This is the step many tutorials skip. WordPress uses the category with the lowest ID (usually the first one created) when building the /%category%/ permalink, unless you use a plugin to choose the primary category. To avoid any guesswork, I made sure every post I wanted to move was assigned only to the Vocabulary category and no other category could hijack the URL.

    How I did it efficiently:

    1. Went to Posts → All Posts
    2. Selected all posts (using the checkbox at the top, then “All X posts” link if needed)
    3. Chose Edit from the Bulk actions dropdown and clicked Apply
    4. In the bulk edit panel, under Categories, I ticked only the “vocabulary” category and un‑ticked any other categories (like “Uncategorized”)
    5. Hit Update

    That guaranteed every single post had exactly one category assignment. No leftovers, no surprise slugs.

    For sites with thousands of posts, you could use a database query or a plugin, but the built‑in bulk edit handled my 80+ posts in seconds.


    Step 3: Switch the Permalink Structure

    With categories now clean, I could safely change the permalink setting.

    1. Went to Settings → Permalinks
    2. Selected Custom Structure and entered: /%category%/%postname%/
    3. Clicked Save Changes

    Instantly, all my post URLs became https://lucidarabic.com/beginner/vocabulary/slug/. The old short URLs started returning 404 errors – completely expected, and completely fixable in the next step.



    Step 4: Generate 301 Redirects in Seconds with WP‑CLI

    wp post list --post_type=post --post_status=publish --format=csv --fields=post_name | tail -n +2 | awk -F, '{print "/" $1 "/,/beginner/vocabulary/" $1 "/"}' > redirects.csv

    It looks dense, but it’s just a chain of three simple commands connected by pipes (|). I’ll walk through each component.


    0. Prerequisites

    • WP‑CLI must be installed on your server (or local environment). Most managed WordPress hosts already have it. You can check by typing wp --info in your terminal.
    • You need SSH access to your server, or you can run it on a local copy of your site (which is safer, and exactly what I did first).
    • Navigate to your WordPress root directory (where wp-config.php lives) before running the command.

    1. wp post list – Extracting the Raw Data

    wp post list --post_type=post --post_status=publish --format=csv --fields=post_name

    This is the WP‑CLI part. It queries the WordPress database for posts and outputs specific information.

    FlagMeaning
    --post_type=postOnly include standard blog posts (exclude pages, attachments, etc.).
    --post_status=publishOnly include published items (no drafts, trashed, or private).
    --format=csvOutput the results as a plain CSV table.
    --fields=post_nameOnly retrieve the post_name field – this is the URL slug.

    Example output of this sub‑command alone:

    post_name
    say-flower-in-arabic
    how-to-say-star-in-arabic
    arabic-word-for-tree
    ...

    The first line is the column header post_name. We need to remove it, which leads to the next command.


    2. tail -n +2 – Removing the Header

    tail -n +2

    tail normally outputs the last lines of a file or input stream. With the -n +2 option, it starts outputting from the second line onward. This effectively drops the first line (the header).

    After this step, the stream becomes:

    say-flower-in-arabic
    how-to-say-star-in-arabic
    arabic-word-for-tree
    ...

    Now we have a clean list of slugs – one per line.


    3. awk – Transforming Each Slug into a CSV Line

    awk -F, '{print "/" $1 "/,/beginner/vocabulary/" $1 "/"}'

    awk is a powerful text processor. Let’s unpack this instruction:

    • -F, sets the field separator to a comma. Although our input has no commas (each line is just a slug), specifying a separator is good practice. Here, $1 represents the entire line because the line contains no comma to split it.
    • '{print ... }' is the action performed on every line.
    • The print statement constructs the exact CSV line we need:
    • "/" adds a leading slash before the slug.
    • $1 inserts the slug itself.
    • "/,/beginner/vocabulary/" adds the comma (CSV delimiter) and the new path prefix.
    • $1 inserts the slug again, after the prefix.
    • "/" adds the trailing slash.

    So, for the input say-flower-in-arabic, the output becomes:

    /say-flower-in-arabic/,/beginner/vocabulary/say-flower-in-arabic/

    And every line is transformed identically.


    4. > redirects.csv – Saving to a File

    The > operator redirects the final output (from awk) into a new file called redirects.csv in the current directory. If the file already exists, it’s overwritten. If you prefer to append, you’d use >>.


    Putting It All Together – A Visual Flow

    [WP Database]
          |
          v
    wp post list …   =>   post_name\nsay-flower-in-arabic\nhow-to-say-star-in-arabic\n…
          |
          v
    tail -n +2       =>   say-flower-in-arabic\nhow-to-say-star-in-arabic\n…
          |
          v
    awk …            =>   /say-flower-in-arabic/,/beginner/vocabulary/say-flower-in-arabic/\n
                          /how-to-say-star-in-arabic/,/beginner/vocabulary/how-to-say-star-in-arabic/\n
                          …
          |
          v
    > redirects.csv   =>   (file saved on disk)

    What the Final CSV Looks Like

    Opening redirects.csv in a text editor, you’ll see lines exactly like:

    /say-flower-in-arabic/,/beginner/vocabulary/say-flower-in-arabic/
    /arabic-word-for-tree/,/beginner/vocabulary/arabic-word-for-tree/
    /how-to-say-star-in-arabic/,/beginner/vocabulary/how-to-say-star-in-arabic/
    /sun-in-arabic/,/beginner/vocabulary/sun-in-arabic/
    …

    Important characteristics:

    • No header row (already stripped).
    • Every line is a valid source/target pair.
    • Both source and target paths are relative and end with a trailing slash – exactly what the Redirection plugin expects.
    • Character encoding is preserved. Slugs that contain percent‑encoded Arabic characters (like %d8%b4) remain untouched, ensuring the redirect matches the actual URL the browser requests.

    Why This Command is So Powerful

    • Zero manual editing: You don’t touch a single URL. The script reads your actual database slugs and builds every mapping automatically.
    • Scalable: Whether you have 10 posts or 10,000, the command runs in the same amount of time (a few seconds).
    • No copy‑paste errors: Manual CSV creation is prone to missing slashes, typos, or duplicate lines. This eliminates human error.
    • Reusable: If you later add an “intermediate/vocabulary” level, you can run the same command with a slightly modified awk pattern.

    Alternatives If You Don’t Have WP‑CLI

    If SSH access or WP‑CLI isn’t available, you can achieve the same result with:

    1. SQL export via phpMyAdmin:
       SELECT CONCAT('/', post_name, '/') AS source,
              CONCAT('/beginner/vocabulary/', post_name, '/') AS target
       FROM wp_posts
       WHERE post_type = 'post' AND post_status = 'publish' AND post_name != '';

    Then export the results as a CSV without headers.

    1. Free export plugin (e.g., WP All Export):
      Export only the post_name field twice, then use spreadsheet formulas to build the source and target columns.

    But WP‑CLI is by far the fastest, and if your host supports it, I highly recommend using it.


    Running It Safely

    I can’t stress enough: run this command on a local/staging copy first. That’s what I did. I cloned my production site to my local machine using Local by Flywheel, executed the WP‑CLI command there, verified the CSV, and then repeated the process on the live server. This gave me absolute confidence that nothing would break.

    After generating the file, I opened it with a plain‑text editor (not Excel, which can alter encodings), checked the first few and last few lines, and then imported it straight into the Redirection plugin.


    The whole process – from running the command to seeing the redirect rules live – took less than five minutes. That single line of code turned what could have been a tedious, error‑prone chore into an automated, reliable solution. It’s now my go‑to technique for any bulk URL restructuring in WordPress.


    Step 5: Import the CSV into the Redirection Plugin

    Back in the WordPress admin:

    1. Installed and activated the free Redirection plugin
    2. Went to Tools → Redirection → Import/Export
    3. Uploaded the CSV, mapped Column 1 to Source URL and Column 2 to Target URL
    4. Clicked Import

    All 80+ redirects were instantly created as 301 (Permanent) rules. I tested a few old URLs in a private browsing window – each one cleanly forwarded to the new hierarchical URL with no lag and no intermediate page.


    Step 6: Update the Sitemap (Don’t Forget This!)

    WordPress generated new URLs, but my XML sitemap (managed by an SEO plugin) still contained the old ones. I needed search engines to discover the new structure quickly.

    I went to my SEO plugin’s settings (I use Rank Math, but Yoast has the same feature) and found the Sitemap Settings. I simply clicked Save Changes or triggered a manual sitemap rebuild. The plugin re‑scanned my posts and updated the sitemap with the new /beginner/vocabulary/ URLs.

    I also submitted the updated sitemap to Google Search Console for good measure.


    Bonus: My Safety Net – Local Testing & Backup

    Before touching the live site, I cloned my entire production site to a local environment (using Local by Flywheel). There I ran through every step – category cleanup, permalink change, WP‑CLI redirect generation, and the Redirection import. I could verify that:

    • The new URLs resolved correctly
    • The old URLs redirected without a hitch
    • No weird 404s appeared
    • My theme and plugins played nicely with the hierarchical permalinks

    Once I was confident, I backed up my production site (both files and database). With the backup securely stored, I repeated the steps on the live server. The whole live migration took about 10 minutes, and because the redirects were already mapped in the CSV, there was zero downtime.


    Key Takeaways

    1. Clean up category assignments before changing permalinks. If you don’t, you’ll end up with unpredictable URLs.
    2. WP‑CLI is your best friend for bulk redirect generation. A single command can save hours of manual CSV editing.
    3. The Redirection plugin makes redirect management painless – and its CSV import works flawlessly.
    4. Always test on a local or staging site first, and have a full backup before making structural changes to a live site.
    5. Update your sitemap immediately so search engines index the new URLs and pass ranking signals through the redirects.

    The result? My Arabic learning site now has a clean, scalable URL structure ready for intermediate and advanced content. Old links still work perfectly, and search engine rankings held steady.

    If you’re facing a similar URL restructuring, don’t fear the change – just plan carefully, let automation do the heavy lifting, and always protect yourself with a backup. And if you have access to the command line, give that WP‑CLI one‑liner a try – you’ll be amazed at how fast it works.


    Have you restructured your WordPress URLs? I’d love to hear about your experience – and any other clever WP‑CLI tricks you’ve used – in the comments below.