Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

HTML

            
              <div class="container mt-4 mb-2" style="max-width: 600px">
  <h2>Human Feed</h2>
  <p class="lead">This is a demo of human-friendly dates created as a companion
  to a PhraseApp blog post. The localized date strings in the feed below have been dynamically
    generated from <code>Date</code> objects. </p>
  
  <div class="mx-auto text-center">
    <p class="lead mb-1">View in</p>
    
    <div class="btn-group mb-2" id="localeControls">
      <button
        id="localeControl-en"
        style="width: 4rem"
        class="btn btn-sm btn-secondary active"
        aria-pressed="true"
      >
        English
      </button>
      
      <button
        id="localeControl-ar"
        style="width: 4rem"
        class="btn btn-sm btn-secondary"
      >
        عربي
      </button>
    </div>
  </div>
  
  <h3 id="feedHeader">What's New</h3>
  
  <ul class="list-group pr-0" id="feed">
  </ul>
</div>
            
          
!

CSS

            
              
            
          
!

JS

            
              // ----------------------------------------------------------------------
// CONFIG
// ----------------------------------------------------------------------

/**
 * Past this number of days we'll no longer display the 
 * day of the week and instead we'll display the date
 * with the month
 */
const DATE_WITH_MONTH_THRESHOLD_IN_DAYS: number = 6;

/**
 * Past this number of seconds it's now longer "now" when
 * we're displaying dates
 */
const NOW_THRESHOLD_IN_SECONDS: number = 10;

/**
 * Past this number of hours we'll no longer display "hours
 * ago" and instead we'll display "today"
 */
const TODAY_AT_THRESHOLD_IN_HOURS: number = 12;

/**
 * Date formats for different human-friendly thresholds
 */
const FORMATS = {
  YEARS_AGO: {
    year: "numeric",
    month: "short",
    day: "numeric",
  },
  
  DAYS_AGO: {
    weekday: "long",
  },
};

// ----------------------------------------------------------------------
// TYPES
// ----------------------------------------------------------------------

/**
 * Supported locales
 */
type Locale = "en" | "ar";

/**
 * Keys allowed in plural forms
 */
type PluralFormKey = "1" | "2" | "3-10" | "other";

/**
 * Definition of a phrase's plural form
 */
interface PluralForms  {
    "1"?: string;
    "2"?: string;
    "3-10"?: string;
    "other": string;
}

/**
 * Translation key/value pairs for supported locales
 */
type Translations = {
  [locale in Locale]:  {
    [key: string]: string | PluralForms;
  }
}

/**
 * Representation of a date & time in components
 */
interface DateTimeComponents {
  years: number;
  months: number;
  days: number;
  hours: number;
  minutes: number;
  seconds: number;
}

/**
 * Options when formatting a date
 */
interface DateFormatOptions {
  includeYear?: Boolean;
}

/**
 * A post in our data set
 */
interface Post {
    name: string;
    photo: string;
    post: string;
    posted_at: Date;
}

// ----------------------------------------------------------------------
// CONSTANTS
// ----------------------------------------------------------------------

const MILLISECONDS_TO_SECONDS: number = 0.001;

const SECONDS_IN_YEAR: number = 31557600;

const SECONDS_IN_MONTH: number = 2629800;

const SECONDS_IN_DAY: number = 86400;

const SECONDS_IN_HOUR: number = 3600;

const SECONDS_IN_MINUTE: number = 60;

// ----------------------------------------------------------------------
// i18n
// ----------------------------------------------------------------------

/**
 * Determines localized UI elements and data shown
 */
let currentLocale: Locale = "en";

/**
 * Localized UI strings
 */
const translations: Translations = { 
  "en": {
    "What's New": "What's New",
    "posted": "posted",
    "just now": "just now",
    "today at": "today at",
    "yesterday": "yesterday",
    "hours ago": {
      "1": "{count} hour ago",
      "other": "{count} hours ago",
    },
    "minutes ago": {
      "1": "{count} minute ago",
      "other": "{count} minutes ago",
    },
    "seconds ago": {
      "1": "{count} second ago",
      "other": "{count} seconds ago",
    },
  },
  
  "ar": {
    "What's New": "ما الجديد",
    "posted": "نشر/ت",
    "just now": "الآن",
    "today at": "اليوم الساعة",
    "yesterday": "الأمس",
    "hours ago": {
      "1": "من ساعة",
      "2": "من ساعتان",
      "3-10": "من {count} ساعات",
      "other": "من {count} ساعة",
    },
    "minutes ago": {
      "1": "من دقيقة",
      "2": "من دقيقتين",
      "3-10": "من {count} دقائق",
      "other": "من {count} دقيقة",
    },
    "seconds ago": {
      "1": "من ثانية",
      "2": "من ثانيتين",
      "3-10": "من {count} ثواني",
      "other": "من {count} ثانية",
    },
  },
};

/**
 * Retrieve a localized string by key
 */
function __(key: string, locale: Locale = currentLocale): string {
  return translations[locale][key] as string;
}

/**
 * Retrieve a plural form for a phrase by the phrase's key
 */
function _p(key: string, count: number, locale: Locale = currentLocale): string {
  const forms = translations[locale][key] as PluralForms;
  
  const { other, ...definiteForms } = forms;
  
  const sortedDefiniteForms = Object.keys(definiteForms).sort();
  
  let hit: string = "";
  
  for (let i: number = 0; i < sortedDefiniteForms.length; i += 1) {
    const currentFormKey: PluralFormKey = sortedDefiniteForms[i] as PluralFormKey;
    
    if (currentFormKey.includes("-")) {
      const [lowerLimit, upperLimit] = currentFormKey.split("-").map(s => parseInt(s.trim(), 10));
      
      if (lowerLimit <= count && count <= upperLimit) {
        hit = forms[currentFormKey]!;
      }
    } else {
      if (count === parseInt(currentFormKey, 10)) {
        hit = forms[currentFormKey]!;
      }
    }
  }
  
  if (hit === "") {
    hit = other || key;
  }
  
  const normalized: string = hit.replace("{count}", count.toString());
  
  return                      locale.toLowerCase().startsWith("ar") ?
     // Convert Arabic numerals to Indian numerals
     normalized.replace(/\d/g, d =>  "٠١٢٣٤٥٦٧٨٩"[parseInt(d, 10)]) :
                                                         normalized ;
}

// ----------------------------------------------------------------------
// HUMAN-FRIENDLY DATES
// ----------------------------------------------------------------------

/**
 * Retrieve a human-friendly date string relative to now and in the
 * current locale e.g. "two minutes ago"
 */
function humanFriendlyDate(date: Date): string {
  const unixTimestamp: number = millisecondsToSeconds(date.valueOf());
  
  const now: number = millisecondsToSeconds(Date.now());
  
  const diffComponents: DateTimeComponents = getDateTimeComponents(now - unixTimestamp);
  
  const { years, months, days, hours, minutes, seconds } = diffComponents;
  
  if (years > 0) {
    return formatLocalizedDateWithOrdinal(currentLocale, date, { includeYear: true });
  }
  
  if (months > 0 || days > DATE_WITH_MONTH_THRESHOLD_IN_DAYS) {
    return formatLocalizedDateWithOrdinal(currentLocale, date, { includeYear: false });
  }
  
  if (days > 1) {
    return date.toLocaleDateString(currentLocale, { weekday: "long" }); 
  }
  
  if (days === 1) {
    return __("yesterday");
  }
  
  if (hours > TODAY_AT_THRESHOLD_IN_HOURS) {
    return __("today at") + " " + 
      date.toLocaleTimeString(currentLocale, { hour: "numeric", minute: "2-digit" }); 
  }
  
  if (hours > 0) {
    return _p("hours ago", hours);
  }
  
  if (minutes > 0) {
    return _p("minutes ago", minutes);
  }
  
  if (seconds > NOW_THRESHOLD_IN_SECONDS) {
    return _p("seconds ago", seconds);
  }
  
  return __("just now");
}

/**
 * For English, format a date with given options, adding an ordinal
 * e.g. "May 1st, 1992" (note the "1st"). For non-English locales,
 * format a date with given options (and no ordinal);
 */
function formatLocalizedDateWithOrdinal(locale: Locale, date: Date, options: DateFormatOptions = { includeYear: false }) {
  if (locale.toLowerCase().startsWith("en")) {
    return formatEnglishDateWithOrdinal(date, options);
  }

  return formatNonEnglishDate(locale, date, options);
}

/**
 * Format an English date with it ordinal e.g. "May 1st, 1992"
 */
function formatEnglishDateWithOrdinal(date: Date, { includeYear }: DateFormatOptions): string {
  const month: string = date.toLocaleDateString("en", { month: "long" });

  const day: string = getOrdinal(date.getDate());

  let formatted: string = `${month} ${day}`;

  if (includeYear) {
    formatted += `, ${date.getFullYear()}`;
  }
  
  return formatted; 
}

/**
 * Format a non-English date
 */
function formatNonEnglishDate(locale: Locale, date: Date, { includeYear }: DateFormatOptions): string {
  const options: Intl.DateTimeFormatOptions = { day: "numeric", month: "long" };
  
  if (includeYear) {
    options.year = "numeric";
  }
  
  return date.toLocaleDateString(locale, options);
}

/**
 * Retrieve an English ordinal for a number, e.g. "2nd" for 2
 */
function getOrdinal(n: number): string {
  // From https://community.shopify.com/c/Shopify-Design/Ordinal-Number-in-javascript-1st-2nd-3rd-4th/m-p/72156
   var s=["th","st","nd","rd"],
       v=n%100;
   return n+(s[(v-20)%10]||s[v]||s[0]);
}

/**
 * Convert milliseconds to seconds
 */
function millisecondsToSeconds(milliseconds: number): number {
  return Math.floor(milliseconds * MILLISECONDS_TO_SECONDS);
}

/**
 * Break up a unix timestamp into its date & time components
 */
function getDateTimeComponents(timestamp: number): DateTimeComponents {
  const components: DateTimeComponents = {
        years: 0,
       months: 0,
         days: 0,
        hours: 0,
      minutes: 0,
      seconds: 0,
  };
  
  let remaining: number = timestamp;

  // years
  components.years = Math.floor(remaining / SECONDS_IN_YEAR);
  
  remaining -= components.years * SECONDS_IN_YEAR;
  
  // months
  components.months = Math.floor(remaining / SECONDS_IN_MONTH);
  
  remaining -= components.months * SECONDS_IN_MONTH;
  
  // days
  components.days = Math.floor(remaining / SECONDS_IN_DAY);
  
  remaining -= components.days * SECONDS_IN_DAY;
  
  // hours
  components.hours = Math.floor(remaining / SECONDS_IN_HOUR);
  
  remaining -= components.hours * SECONDS_IN_HOUR;
  
  // minutes
  components.minutes = Math.floor(remaining / SECONDS_IN_MINUTE);
  
  remaining -= components.minutes * SECONDS_IN_MINUTE;
  
  // seconds
  components.seconds = remaining;
  
  return components;
}

// ----------------------------------------------------------------------
// DATA & RENDERING
// ----------------------------------------------------------------------

/**
 * All dates are displayed relative to this
 */
const baseTime: number = Date.now();

/**
 * Mock English data for our demo
 */
const en_data: Post[] = [
  {
      "name": "Fawzi Abdulrahman",
      "photo": "https://tinyfac.es/data/avatars/475605E3-69C5-4D2B-8727-61B7BB8C4699-500w.jpeg",
      "post": "Gun hornswaggle furl long clothes hands spike hardtack plunder log mizzen. Landlubber or just lubber chandler long clothes jib holystone yard warp lateen sail Cat o'nine tails fore.",
      "posted_at": new Date(baseTime),  
  },
  {
      "name": "June Cha",
      "photo": "https://tinyfac.es/data/avatars/8B510E03-96BA-43F0-A85A-F38BB3005AF1-500w.jpeg",
    "post": "Halvah fruitcake donut brownie chocolate bear claw. Muffin biscuit tootsie roll candy. Cheesecake jelly-o lemon drops sweet chocolate bar marshmallow marshmallow. Cotton candy donut cheesecake sweet.",
    "posted_at": new Date(baseTime - 1 / MILLISECONDS_TO_SECONDS)
  },
  {
      "name": "Iida Niskanen",
      "photo": "https://tinyfac.es/data/avatars/BA0CB1F2-8C79-4376-B13B-DD5FB8772537-200w.jpeg",
    "post": "Melted cheese bocconcini mozzarella. Halloumi swiss pecorino dolcelatte monterey jack cream cheese cheese triangles dolcelat",
    "posted_at": new Date(baseTime - 30 / MILLISECONDS_TO_SECONDS),
  },
  {
      "name": "Renee Sims",
      "photo": "https:\/\/randomuser.me\/api\/portraits\/women\/65.jpg",
      "post": "Check it out, y'all. Everyone who was invited is here. So, how 'bout them Knicks? Why would a robot need to drink?",
      "posted_at": new Date(baseTime - 2 * SECONDS_IN_MINUTE / MILLISECONDS_TO_SECONDS),
  },
  {
      "name": "Jonathan Nu\u00f1ez",
      "photo": "https:\/\/randomuser.me\/api\/portraits\/men\/43.jpg",
      "post": "Viral air plant aesthetic, williamsburg iPhone glossier vape. Try-hard hell of chicharrones aesthetic.",
      "posted_at":   new Date(baseTime - 1 * SECONDS_IN_HOUR / MILLISECONDS_TO_SECONDS),  
  },
  {
      "name": "Sasha Ho",
      "photo": "https:\/\/images.pexels.com\/photos\/415829\/pexels-photo-415829.jpeg?h=350&auto=compress&cs=tinysrgb",
    "post": "What's the status on the deliverables for eow? root-and-branch review best practices, or if you want to motivate these clowns, try less carrot and more stick.",
    "posted_at": new Date(baseTime - 13 * SECONDS_IN_HOUR / MILLISECONDS_TO_SECONDS),
  },
  {
      "name": "Abdullah Hadley",
      "photo": "https:\/\/images.unsplash.com\/photo-1507003211169-0a1dd7228f2d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=a72ca28288878f8404a795f39642a46f",
    "post": "Tunguska event Euclid two ghostly white figures in coveralls and helmets are soflty dancing Apollonius of Perga hundreds of thousands intelligent beings?",
    "posted_at": new Date(baseTime - 1 * SECONDS_IN_DAY / MILLISECONDS_TO_SECONDS),   
  },
  {
      "name": "Veeti Seppanen",
      "photo": "https:\/\/randomuser.me\/api\/portraits\/men\/97.jpg",
      "post": "De carne lumbering animata corpora quaeritis. Summus brains sit​​, morbo vel maleficia?",
      "posted_at": new Date(baseTime - 2 * SECONDS_IN_DAY / MILLISECONDS_TO_SECONDS),
  },
  {
      "name": "Thomas Stock",
      "photo": "https:\/\/tinyfac.es\/data\/avatars\/B0298C36-9751-48EF-BE15-80FB9CD11143-500w.jpeg",
    "post": "Ignore the squirrels, you'll never catch them anyway vommit food and eat it again.",
    "posted_at": new Date(baseTime - 7 * SECONDS_IN_DAY / MILLISECONDS_TO_SECONDS), 
  },
  {
      "name": "Bonnie Riley",
      "photo": "https:\/\/randomuser.me\/api\/portraits\/women\/26.jpg",
    "post": "No borders, no limits… go ahead, touch the Cornballer… you know best? Steve Holt? The moron jock? I am going to my spin class.",
    "posted_at": new Date(baseTime - 1 * SECONDS_IN_MONTH / MILLISECONDS_TO_SECONDS),   
  },
  {
        "name": "Abdullah Hadley",
        "photo": "https:\/\/images.unsplash.com\/photo-1507003211169-0a1dd7228f2d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=a72ca28288878f8404a795f39642a46f",
      "post": "At the pump-room I saw Mr. Macartney; I courtsied to him twice ere he would speak to me. ",
      "posted_at": new Date(baseTime - 5 * SECONDS_IN_MONTH / MILLISECONDS_TO_SECONDS),  
    },
  {
        "name": "Sasha Ho",
        "photo": "https:\/\/images.pexels.com\/photos\/415829\/pexels-photo-415829.jpeg?h=350&auto=compress&cs=tinysrgb",
      "post": "Hand-crafted sleepy Asia-Pacific elegant Scandinavian Zürich Marylebone carefully curated Sunspel essential ryokan. Delightful flat white bespoke hub essential espresso Porter Melbourne Sunspel wardrobe.",
      "posted_at": new Date(baseTime - 1 * SECONDS_IN_YEAR / MILLISECONDS_TO_SECONDS),
    },
];

/**
 * Mock Arabic data for our demo
 */
const ar_data: Post[] = [
  {
      "name": "فوزي عبدالرحمان",
      "photo": "https://tinyfac.es/data/avatars/475605E3-69C5-4D2B-8727-61B7BB8C4699-500w.jpeg",
      "post": "بندقية hornswaggle furl ملابس طويلة الأيدي سبايك hardtack نهب سجل mizzen. Landlubber أو مجرد lubber chandler ملابس طويلة jib holystone yard warp lateen الشراع Cat o'nine ذيول الصدارة.",
      "posted_at": new Date(baseTime),  
  },
  {
      "name": "جون شا",
      "photo": "https://tinyfac.es/data/avatars/8B510E03-96BA-43F0-A85A-F38BB3005AF1-500w.jpeg",
    "post": "الحلاوة فواكه الكعك دونات براوني الشوكولاته الدب مخلب. الكعك بسكويت tootsie لفة الحلوى. تشيز كيك جيلي-ليمون يسقط حلوى الشوكولاته بار الخطمي الخطمي. حلوى القطن دونات تشيز كيك حلوة",
    "posted_at": new Date(baseTime - 1 / MILLISECONDS_TO_SECONDS)
  },
  {
      "name": "ايدا نسكانين",
      "photo": "https://tinyfac.es/data/avatars/BA0CB1F2-8C79-4376-B13B-DD5FB8772537-200w.jpeg",
    "post": "ذاب الجبن بوكونسيني موزاريلا. حلومي سويس بيكورينو دولسيلاتي مونتيري جاك جبنة مثلثات دولسيلات",
    "posted_at": new Date(baseTime - 30 / MILLISECONDS_TO_SECONDS),
  },
  {
      "name": "ريني سيمز",
      "photo": "https:\/\/randomuser.me\/api\/portraits\/women\/65.jpg",
      "post": "التحقق من ذلك ، كل شيء. كل من تمت دعوته هنا. لذا ، كيف نوبة لهم نيكس؟ لماذا يحتاج الروبوت للشرب؟",
      "posted_at": new Date(baseTime - 2 * SECONDS_IN_MINUTE / MILLISECONDS_TO_SECONDS),
  },
  {
      "name": "جوناثان نونز",
      "photo": "https:\/\/randomuser.me\/api\/portraits\/men\/43.jpg",
      "post": "جمالية الهواء النباتية الفيروسية ، وليامز فون المصقول vape. محاولة من الصعب الجحيم chicharrones الجمالية.",
      "posted_at":   new Date(baseTime - 1 * SECONDS_IN_HOUR / MILLISECONDS_TO_SECONDS),  
  },
  {
      "name": "ساشا هو",
      "photo": "https:\/\/images.pexels.com\/photos\/415829\/pexels-photo-415829.jpeg?h=350&auto=compress&cs=tinysrgb",
    "post": "ما هو الوضع على التسليمات ل eow؟ راجع أفضل الممارسات الخاصة بالجذر والفروع ، أو إذا كنت ترغب في تحفيز هؤلاء المهرجين ، فحاول استخدام عدد أقل من الجزرة والعصا.",
    "posted_at": new Date(baseTime - 13 * SECONDS_IN_HOUR / MILLISECONDS_TO_SECONDS),
  },
  {
      "name": "عبد الله هيدلي",
      "photo": "https:\/\/images.unsplash.com\/photo-1507003211169-0a1dd7228f2d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=a72ca28288878f8404a795f39642a46f",
    "post": "حدث تونجوسكا إقليدس شخصيتان أبيضتان شبحتان في المعاطف والخوذات يرقصان أبولونيوس من بيرغا مئات الآلاف من الكائنات الذكية؟",
    "posted_at": new Date(baseTime - 1 * SECONDS_IN_DAY / MILLISECONDS_TO_SECONDS),   
  },
  {
      "name": "ڤيتي سيبانين",
      "photo": "https:\/\/randomuser.me\/api\/portraits\/men\/97.jpg",
      "post": "De carne lumbering animata corpora quaeritis. أدمغة Summus الجلوس ، morbo vel ينكوليا؟",
      "posted_at": new Date(baseTime - 2 * SECONDS_IN_DAY / MILLISECONDS_TO_SECONDS),
  },
  {
      "name": "توماس ستوك",
      "photo": "https:\/\/tinyfac.es\/data\/avatars\/B0298C36-9751-48EF-BE15-80FB9CD11143-500w.jpeg",
    "post": "تجاهل السناجب ، فلن تصطادهم أبداً بتقيؤ الطعام وتناوله مرة أخرى.",
    "posted_at": new Date(baseTime - 7 * SECONDS_IN_DAY / MILLISECONDS_TO_SECONDS), 
  },
  {
      "name": "بوني رايلي",
      "photo": "https:\/\/randomuser.me\/api\/portraits\/women\/26.jpg",
    "post": "لا حدود ، لا حدود ... المضي قدما ، المس Cornballer ... أنت تعرف أفضل؟ ستيف هولت؟ جوك معتوه؟ أنا ذاهب إلى صفي تدور.",
    "posted_at": new Date(baseTime - 1 * SECONDS_IN_MONTH / MILLISECONDS_TO_SECONDS),   
  },
  {
      "name": "عبد الله هيدلي",
        "photo": "https:\/\/images.unsplash.com\/photo-1507003211169-0a1dd7228f2d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=a72ca28288878f8404a795f39642a46f",
      "post": "في غرفة المضخة ، رأيت السيد مكارتني. كنت مغازلة له مرتين إذا كان سيتحدث معي. ",
      "posted_at": new Date(baseTime - 5 * SECONDS_IN_MONTH / MILLISECONDS_TO_SECONDS),  
    },
  {
      "name": "ساشا هو",
        "photo": "https:\/\/images.pexels.com\/photos\/415829\/pexels-photo-415829.jpeg?h=350&auto=compress&cs=tinysrgb",
      "post": "صُنعت الاسكندنافية زيوريخ ماريليبوني الأنيقة المصنّعة يدوياً في منطقة آسيا والمحيط الهادئ برعاية Sunspel الأساسية. مبهج مفصل أبيض مسطح محور إسبرسو الأساسية بورتر ملبورن Sunspel خزانة الملابس.",
      "posted_at": new Date(baseTime - 1 * SECONDS_IN_YEAR / MILLISECONDS_TO_SECONDS),
    },
];

/**
 * Retrieve HTML for a single post, humanizing the post's date
 */
function renderPost(post: Post): string {
  const avatarMargin = currentLocale === "en" ? "mr-3" : "ml-3";
  
  return ( 
  `<li class="list-group-item">
    <div class="d-flex">
      <div 
        class="${avatarMargin} rounded-circle"
        style="width: 4rem; height: 4rem; overflow: hidden;"
      >
        <img src="${post.photo}"
            style="object-fit: cover; object-position: center; min-height: 100%; width: 100%;"
        />
    </div>

    <div style="flex: 1;">
      <h4 class="h6 d-flex justify-content-between align-items-baseline">
        <span>${post.name} ${__("posted")}</span>
        <span class="small">${humanFriendlyDate(post.posted_at)}</span>
      </h4>

      <p style="text-align: ${currentLocale === "en" ? "left" : "right"}">
        ${post.post}
      </p>

      </div>
    </div>
  </li>`
    );
}

/**
 * Update button state based on active button
 */
function updateLocaleButtons(): void {
  const buttons = {
    en: document.getElementById("localeControl-en"),
    ar: document.getElementById("localeControl-ar"),
  }
  
  Object.keys(buttons).forEach((locale) => {
    const btn = buttons[locale as Locale];
    
    if (btn === null) { return; }

    btn.setAttribute("aria-pressed", "false");
    btn.className = btn.className.replace("active", "");
  });
  
  const selectedBtn = buttons[currentLocale];

  if (selectedBtn === null) { return; }
  
  selectedBtn.setAttribute("aria-pressed", "true");
  selectedBtn.className += " active";
}

/**
 * Refresh UI based on selected locale
 */
function selectLocale(newLocale: Locale): void {
  currentLocale = newLocale;
  
  const data = newLocale === "en" ? en_data : ar_data;
  
  const feed = document.getElementById('feed');

  if (feed !== null) {
    feed.dir = newLocale === "en" ? "ltr" : "rtl";
  
    feed.innerHTML = data.map(renderPost).join("");
  }
  
  const feedHeader = document.getElementById("feedHeader");

  if (feedHeader !== null) {
    feedHeader.innerHTML = __("What's New");
  
    feedHeader.style.textAlign = newLocale === "en" ? "left" : "right";
  }
  
  updateLocaleButtons();
}

/**
 * Locale-switcher
 */
const localeControls = document.getElementById('localeControls');

if (localeControls !== null) {
  // Switch locale when respective button is clicked
  localeControls.addEventListener("click", e => {
    const btn = e.target as HTMLButtonElement;

    if (btn === null) { return; }

    const newLocale: Locale = btn.id.split("-")[1].trim() as Locale;
    
    selectLocale(newLocale);
  });
}

selectLocale("en");

            
          
!
999px

Console