<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Henry Lim</title>
    <description>The latest articles on Forem by Henry Lim (@henrylim96).</description>
    <link>https://forem.com/henrylim96</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4625%2F7caac3f9-4594-4e71-a79b-0fa8a10b6e6c.jpg</url>
      <title>Forem: Henry Lim</title>
      <link>https://forem.com/henrylim96</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/henrylim96"/>
    <language>en</language>
    <item>
      <title>5 Game-Changing Chrome DevTools Updates You Need to Try in 2025</title>
      <dc:creator>Henry Lim</dc:creator>
      <pubDate>Fri, 05 Dec 2025 22:31:27 +0000</pubDate>
      <link>https://forem.com/henrylim96/5-game-changing-chrome-devtools-updates-you-need-to-try-in-2025-2mal</link>
      <guid>https://forem.com/henrylim96/5-game-changing-chrome-devtools-updates-you-need-to-try-in-2025-2mal</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is an article version of my talk &lt;a href="http://go.limhenry.xyz/devfest-bhm" rel="noopener noreferrer"&gt;&lt;strong&gt;“Chrome DevTools LIVE LIVE LIVE”&lt;/strong&gt;&lt;/a&gt; presented at &lt;a href="https://devfest25.gdgvenezia.it/" rel="noopener noreferrer"&gt;DevFest Venice 2025&lt;/a&gt; and &lt;a href="https://gdg.community.dev/events/details/google-gdg-birmingham-presents-devfest-birmingham-2025/" rel="noopener noreferrer"&gt;DevFest Birmingham 2025&lt;/a&gt;.*&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;In this article, we dive into the latest updates in Chrome DevTools designed to help developers cut through the complexity of auditing web pages for performance. We'll explore how Live Metrics Page offers instant, user-centric data; how CPU and Network Throttling can be used to accurately simulate real-world device performance and connection conditions; and how AI is finally making the notorious performance "waterfall" readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live Metrics Page
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx5esx2pmlm3efsasfp0e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx5esx2pmlm3efsasfp0e.png" alt="Cartoon illustration titled "&gt;&lt;/a&gt;&lt;/p&gt;
(Credit: &lt;a href="https://simply-the-test.blogspot.com/2010/05/it-works-on-my-machine.html" rel="noopener noreferrer"&gt;"IWOMM - It Works on My Machine" by Torsten J. Zelger&lt;/a&gt;)



&lt;p&gt;"Hey, the website feels very snappy on my computer," says the developer using an M5 MacBook Pro with 32GB of RAM, costing 2,299 Euro.&lt;/p&gt;

&lt;p&gt;It is important to know the real-world performance results for your users. Thankfully, Chrome collects this data globally in the &lt;a href="https://developer.chrome.com/docs/crux" rel="noopener noreferrer"&gt;Chrome User Experience Report (CrUX)&lt;/a&gt;. This report measures essential web performance metrics, known as &lt;a href="https://web.dev/articles/vitals" rel="noopener noreferrer"&gt;Core Web Vitals&lt;/a&gt;, such as Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS).&lt;/p&gt;

&lt;p&gt;With that, we can use the Live Metrics Page to get a high-level understanding of web performance by opening the Performance panel. The metrics data is live, as the INP and CLS scores will keep updating as you interact with the page. It will also point out your LCP element, identify which element is causing CLS, and show the history of interactions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlcd6063o89b1bpd2cfn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlcd6063o89b1bpd2cfn.png" alt="Screenshot of the Live Metrics view in the Performance Panel displaying real-time LCP, CLS, and INP metrics and an interactions log"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CPU Throttling Calibration
&lt;/h2&gt;

&lt;p&gt;One way to simulate real user environments for accurate local metrics is to use CPU throttling. &lt;a href="https://umaar.com/dev-tips/88-cpu-throttling/" rel="noopener noreferrer"&gt;DevTools added CPU throttling back in 2015&lt;/a&gt;; however, the current 4x or 20x (CPU) slowdown options are quite limited, making it difficult to accurately simulate real-world performance. For example, with the 20x CPU slowdown throttling selected, your expensive M5 MacBook Pro could be faster than the performance of your user’s device.&lt;/p&gt;

&lt;p&gt;In Chrome 134, &lt;a href="https://developer.chrome.com/blog/devtools-grounded-real-world" rel="noopener noreferrer"&gt;DevTools added CPU throttling calibration&lt;/a&gt;, which allows you to simulate CPU throttling with a more accurate result. By running the CPU throttling calibration, DevTools will generate "low-tier mobile" and "mid-tier mobile" throttling presets, specific to your development machine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjx4dnome4qno46mco2ec.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjx4dnome4qno46mco2ec.png" alt="Screenshot of the Throttling settings in DevTools showing the Calibrate button and generated mobile tier presets"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Individual Request Throttling
&lt;/h2&gt;

&lt;p&gt;Network Throttling &lt;a href="https://www.youtube.com/watch?v=t1PxmN_QNFI&amp;amp;t=1041s" rel="noopener noreferrer"&gt;was added to Chrome DevTools back in 2014&lt;/a&gt;. For the past 11+ years, network throttling has always applied to all network requests.&lt;/p&gt;

&lt;p&gt;As of November 2025, &lt;a href="https://www.debugbear.com/blog/chrome-devtools-throttle-individual-request" rel="noopener noreferrer"&gt;Individual Request Throttling has been added to Chrome Canary&lt;/a&gt;. With this feature, you can now throttle a specific network request to a specific speed. You can customize the request in the “Request conditions” tab (previously called “Network request blocking”). &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0g5il9vzbzaa58p0u3b6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0g5il9vzbzaa58p0u3b6.png" alt="Screenshot of the Request Conditions tab in DevTools showing a rule to throttle a specific URL"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can use &lt;a href="https://developer.chrome.com/docs/devtools/settings/throttling#network-throttling" rel="noopener noreferrer"&gt;network throttling profiles&lt;/a&gt; to create a custom network throttling profile, allowing you to customize download/upload speeds, network latency, packet loss percentage, and packet queue length.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fculscez6d0wgw15rljzc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fculscez6d0wgw15rljzc.png" alt="Screenshot of the Network Throttling Profiles settings allowing configuration of custom download, upload, and latency values"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To enable Individual Request Throttling, you need to install the latest version of Chrome Canary and enable &lt;strong&gt;“Enable individual request throttling in DevTools”&lt;/strong&gt; in &lt;code&gt;chrome://flags&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example use case for Individual Request Throttling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scenario:&lt;/strong&gt; You want to test if your web font loading strategy prevents "Flash of Invisible Text" (FOIT), but you don't want to slow down your JS bundles.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to test it:&lt;/strong&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;strong&gt;Network Panel&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;Right-click a specific resource (e.g., &lt;code&gt;roboto.woff2&lt;/code&gt;).
&lt;/li&gt;
&lt;li&gt;Select "&lt;strong&gt;Throttle requests"&lt;/strong&gt; &amp;gt; &lt;strong&gt;“Throttle request URL”&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;In the “&lt;strong&gt;Request conditions”&lt;/strong&gt; tab, change the network preset as you like, “Slow 4G” for example.
&lt;/li&gt;
&lt;li&gt;Refresh the page, notice the color for &lt;code&gt;roboto.woff2&lt;/code&gt; in Network Panel has now changed to yellow.
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By only slowing down the &lt;code&gt;roboto.woff2&lt;/code&gt;, you can now check if your &lt;code&gt;font-display: swap&lt;/code&gt; fallback strategy is working correctly without affecting the rest of the page to load.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Assistance for Performance
&lt;/h2&gt;

&lt;p&gt;The "Waterfall" (or inverted &lt;a href="https://www.datadoghq.com/knowledge-center/distributed-tracing/flame-graph/" rel="noopener noreferrer"&gt;flamechart&lt;/a&gt;) in the Performance panel has been notoriously difficult to read and understand. Things just get worse with modern web frameworks, which bombard the waterfall with even more noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; There are two ways to navigate the waterfall using a keyboard: &lt;strong&gt;Modern&lt;/strong&gt; (use scrollwheel to scroll) and &lt;strong&gt;Classic&lt;/strong&gt; (use scrollwheel to zoom). Click the question mark icon in the top right corner in the performance panel.  And use &lt;strong&gt;“W”&lt;/strong&gt; and &lt;strong&gt;“S”&lt;/strong&gt; for zooming, &lt;strong&gt;“A”&lt;/strong&gt; and &lt;strong&gt;“D”&lt;/strong&gt; for panning left or right.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F259mln6qv1x2zy2hql0o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F259mln6qv1x2zy2hql0o.png" alt="Screenshot of the Performance panel help dialog showing keyboard shortcuts for panning and zooming the waterfall"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Another Tip:&lt;/strong&gt; If you are using &lt;a href="https://blog.angular.dev/the-angular-custom-profiling-track-is-now-available-0f9d8d36218a" rel="noopener noreferrer"&gt;Angular v20&lt;/a&gt; or &lt;a href="https://react.dev/blog/2025/10/01/react-19-2#performance-tracks" rel="noopener noreferrer"&gt;React 19.2&lt;/a&gt;, you will now see &lt;a href="https://developer.chrome.com/docs/devtools/performance/extension" rel="noopener noreferrer"&gt;custom tracks&lt;/a&gt; that provide more detailed information about your Angular/React app's performance thanks to the console.timeStamp API and User Timings API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkms5md6nsy76fr977nf0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkms5md6nsy76fr977nf0.png" alt="Screenshot of a Performance trace timeline highlighting a custom framework track with detailed component timings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  AI Annotation
&lt;/h3&gt;

&lt;p&gt;Understanding the waterfall is the first step to analyze web performance. Annotation on the waterfall could be very useful, as you can label the track with your own words. This is also very useful if you need to share the findings with your colleagues. With the help of AI, DevTools can automatically generate the label for you based on the context. &lt;/p&gt;

&lt;p&gt;To do this, double-click on one of the main tracks, then click on &lt;strong&gt;"Generate label"&lt;/strong&gt;. AI will suggest a label based on the stack trace. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foh723xl798uszdlzairx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foh723xl798uszdlzairx.png" alt="Screenshot of a right-click context menu in the Performance waterfall showing the Generate label option"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Debug with AI
&lt;/h3&gt;

&lt;p&gt;In the AI assistance panel, you can simply ask “What performance issues exist with my page?”, and you will be able to look at the potential performance issues from a higher level before diving deeper.&lt;/p&gt;

&lt;p&gt;Or in the performance waterfall, right-click on any stack, then you can use AI to assess the purpose, identify time spent, find improvements, or just ask AI any question in the chat (AI assistance panel) in plain language&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This feature was previously called “Ask AI”, &lt;a href="https://developer.chrome.com/blog/new-in-devtools-142#ai-assistance" rel="noopener noreferrer"&gt;it was renamed to “Debug with AI” in Chrome 142&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F16crjhtsm3e1zfngvsmq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F16crjhtsm3e1zfngvsmq.png" alt="Screenshot of the AI Assistance panel in DevTools showing a chat conversation analyzing page performance issues"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Chrome DevTools MCP
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/ChromeDevTools/chrome-devtools-mcp/" rel="noopener noreferrer"&gt;Chrome DevTools MCP&lt;/a&gt; is the most powerful feature added to Chrome DevTools this year. It allows your coding agent (such as Gemini CLI, Copilot, Cursor, etc) to control and inspect a live Chrome browser, giving your AI coding assistant access to the full power of Chrome DevTools for reliable automation, in-depth debugging, and performance analysis.&lt;/p&gt;

&lt;p&gt;This is significantly more powerful than the built-in AI assistance within Chrome DevTools because it has direct access to your source code. Previously, the AI suggestions from your IDE were generated without runtime context. Now with Chrome DevTools MCP, it can cross-reference your source code with live browser behavior, including performance trace, network requests, and console errors.&lt;/p&gt;

&lt;p&gt;For example, you can ask the AI, &lt;code&gt;localhost:8080 is loading slowly. Make it load faster&lt;/code&gt;. Chrome DevTools MCP will start a performance trace and use &lt;code&gt;performance_analyze_insight&lt;/code&gt; to gather Core Web Vitals metrics like LCP, CLS, and INP.&lt;/p&gt;

&lt;p&gt;Since it also has access to your source code, it can pinpoint the culprit and provide accurate fixes. It can even re-measure performance after the fix to verify that the changes actually worked.&lt;/p&gt;

&lt;p&gt;

&lt;iframe class="tweet-embed" id="tweet-1970505063064825994-323" src="https://platform.twitter.com/embed/Tweet.html?id=1970505063064825994"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1970505063064825994-323');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1970505063064825994&amp;amp;theme=dark"
  }





&lt;/p&gt;

&lt;p&gt;Performance auditing is just one use case for the Chrome DevTools MCP. &lt;a href="https://addyosmani.com/blog/devtools-mcp/" rel="noopener noreferrer"&gt;Addy Osmani’s article covers even more scenarios&lt;/a&gt;, such as verifying code changes, performing network diagnosis, simulating user behavior for testing, and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In conclusion, optimizing web performance is very important for providing a good user experience. By using these 5 game-changing DevTools features, including the Live Metrics Page, advanced throttling options, and powerful AI Assistance, you can move beyond local benchmarks to accurately understand and optimize Core Web Vitals for every user.&lt;/p&gt;

&lt;p&gt;However, these features are just the tip of the iceberg; Chrome DevTools is significantly more powerful than you might expect. With the Chrome DevTools MCP still improving, I can’t wait to see more advanced use cases emerge that redefine performance auditing.&lt;/p&gt;




&lt;p&gt;Find me on social media!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LinkedIn: &lt;a href="https://www.linkedin.com/in/henrylim96/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/henrylim96/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bluesky: &lt;a href="https://bsky.app/profile/henrylim96.bsky.social" rel="noopener noreferrer"&gt;https://bsky.app/profile/henrylim96.bsky.social&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Website: &lt;a href="https://limhenry.xyz/" rel="noopener noreferrer"&gt;https://limhenry.xyz/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>webperf</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
    <item>
      <title>Turn Any Bluetooth Speaker into an AirPlay Speaker with Shairport Sync</title>
      <dc:creator>Henry Lim</dc:creator>
      <pubDate>Fri, 29 Aug 2025 20:34:22 +0000</pubDate>
      <link>https://forem.com/henrylim96/turn-any-bluetooth-speaker-into-an-airplay-speaker-with-shairport-sync-177j</link>
      <guid>https://forem.com/henrylim96/turn-any-bluetooth-speaker-into-an-airplay-speaker-with-shairport-sync-177j</guid>
      <description>&lt;p&gt;Got a mini-PC, SBC, or Raspberry Pi speaker lying around? You can pair it with a Bluetooth speaker to create your own AirPlay receiver, letting you stream music from any Apple device. This guide uses &lt;a href="https://dietpi.com/" rel="noopener noreferrer"&gt;DietPi&lt;/a&gt; and &lt;a href="https://github.com/mikebrady/shairport-sync" rel="noopener noreferrer"&gt;Shairport Sync&lt;/a&gt; to build a dedicated, budget-friendly audio streamer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgh0yiv9x2uigvczt8bt8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgh0yiv9x2uigvczt8bt8.png" alt="A diagram showing a home audio setup. An " width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What You'll Need
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A Linux-compatible computer (like a mini-PC, SBC, or Raspberry Pi)&lt;/li&gt;
&lt;li&gt;A microSD card with DietPi installed&lt;/li&gt;
&lt;li&gt;A Bluetooth adapter (if your computer doesn't have built-in Bluetooth, a simple one like the &lt;a href="https://www.amazon.com/dp/B07NQ5YGDW" rel="noopener noreferrer"&gt;TP-Link UB400&lt;/a&gt; will work)&lt;/li&gt;
&lt;li&gt;A Bluetooth speaker&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  A Note on DietPi
&lt;/h3&gt;

&lt;p&gt;While you could do this on any Linux distribution like Ubuntu or Debian, DietPi is a great choice because it's lightweight and its user-friendly command-line interface makes installing software and configuring your device much simpler.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Install the Software
&lt;/h3&gt;

&lt;p&gt;First, get your computer ready. After installing DietPi, use its built-in software installer to get the two main programs we need.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Open the DietPi software menu: &lt;code&gt;dietpi-software&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Install &lt;strong&gt;Shairport Sync (AirPlay 2)&lt;/strong&gt; (&lt;code&gt;#37&lt;/code&gt;) and &lt;strong&gt;ALSA&lt;/strong&gt; (&lt;code&gt;#5&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F52xnqvr7x93w8oa0rnyk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F52xnqvr7x93w8oa0rnyk.png" alt="Screenshot of dietpi-software (Shairport Sync)" width="800" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcn9axd734qt41gg5oq7h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcn9axd734qt41gg5oq7h.png" alt="Screenshot of dietpi-software (ALSA)" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Enable Bluetooth and Pair
&lt;/h3&gt;

&lt;p&gt;Next, you need to enable Bluetooth on the computer and pair it with your speaker.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Enable Bluetooth in the DietPi configuration menu: &lt;code&gt;dietpi-config&lt;/code&gt; -&amp;gt; &lt;strong&gt;Advanced Options&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Bluetooth&lt;/strong&gt; -&amp;gt; &lt;strong&gt;[On]&lt;/strong&gt;.
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm5w1ytrase3xz1x8x1va.png" alt="Screenshot of Dietpi-config" width="800" height="301"&gt;
&lt;/li&gt;
&lt;li&gt; Install the necessary utilities: &lt;code&gt;apt install bluez-alsa-utils&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Now, use &lt;code&gt;bluetoothctl&lt;/code&gt; to connect your computer to the speaker. Be sure to replace the example MAC address (&lt;code&gt;02:50:41:A4:DE:6B&lt;/code&gt;) with your speaker's actual address.

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bluetoothctl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;power on&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scan on&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Wait for your Bluetooth speaker's MAC address to appear in the list. Note down the address and then turn off the scan.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scan off&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;trust 02:50:41:A4:DE:6B&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;connect 02:50:41:A4:DE:6B&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; For a cleaner setup, you can rename your computer's Bluetooth broadcast name.

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;system-alias AirPlay # Rename it to "AirPlay"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  3. Configure Audio Routing
&lt;/h3&gt;

&lt;p&gt;To ensure audio is sent through Bluetooth, we'll edit the ALSA configuration file.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Open the file: &lt;code&gt;nano /etc/asound.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Replace the entire content with the following code. This tells ALSA to route all default audio to the Bluetooth speaker.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pcm.btreceiver {
    type plug
    slave.pcm {
        type bluealsa
        device "02:50:41:A4:DE:6B"
        profile "a2dp"
    }
    hint {
        show on
        description "Bluetooth Receiver"
    }
}

pcm.!default {
    type plug
    slave.pcm "btreceiver"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  4. Automate the Connection
&lt;/h3&gt;

&lt;p&gt;To make this setup seamless, we'll create a script that automatically connects to the speaker whenever AirPlay starts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Create the script file: &lt;code&gt;nano /usr/local/bin/shairport-sync-connect.sh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add this simple script to the file:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo "Connecting to Bluetooth Speaker ..."
bluetoothctl connect "02:50:41:A4:DE:6B"
sleep 3
exit 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Make the script executable: &lt;code&gt;chmod +x /usr/local/bin/shairport-sync-connect.sh&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  5. Finalize Shairport Sync Settings
&lt;/h3&gt;

&lt;p&gt;Finally, we'll update the Shairport Sync configuration to use the new script and give your AirPlay device a friendly name.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Open the configuration file: &lt;code&gt;nano /usr/local/etc/shairport-sync.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Find and change these three lines:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;general.name = "Bluetooth Speaker"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sessioncontrol.run_this_before_entering_active_state = "/usr/local/bin/shairport-sync-connect.sh"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sessioncontrol.wait_for_completion = "yes"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyabbova4qgsjuuhjopa0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyabbova4qgsjuuhjopa0.png" alt="Screenshot of Shairport Sync configuration (General Section)" width="800" height="155"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6n9xgzjk6hy8ufj4kms8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6n9xgzjk6hy8ufj4kms8.png" alt="Screenshot of Shairport Sync configuration (Session Control Section)" width="800" height="195"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Restart the Service
&lt;/h3&gt;

&lt;p&gt;To apply the changes you just made, you need to restart the Shairport Sync service.&lt;/p&gt;

&lt;p&gt;Run the following command to restart the service: &lt;code&gt;systemctl restart shairport-sync&lt;/code&gt;&lt;/p&gt;




&lt;p&gt;That's it! Now your computer will appear as an AirPlay speaker on your Apple devices, ready to stream music. Enjoy your new, custom-built sound system!&lt;/p&gt;

</description>
      <category>raspberrypi</category>
      <category>tutorial</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Enhancing Link Shortener App with Google Gemini</title>
      <dc:creator>Henry Lim</dc:creator>
      <pubDate>Sat, 28 Jun 2025 12:35:15 +0000</pubDate>
      <link>https://forem.com/henrylim96/enhancing-link-shortener-app-with-google-gemini-270j</link>
      <guid>https://forem.com/henrylim96/enhancing-link-shortener-app-with-google-gemini-270j</guid>
      <description>&lt;p&gt;In this article, we'll learn how to enhance a link shortener app with Google Gemini. We will use Node.js and Express for our backend, Gemini to generate an easy-to-remember short link, and Puppeteer to extract useful context from web pages.&lt;/p&gt;




&lt;h3&gt;
  
  
  What We'll Learn Today:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;How to set up the &lt;a href="https://www.npmjs.com/package/@google/genai" rel="noopener noreferrer"&gt;Google Gen AI JS/TS SDK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How to use Gemini to &lt;a href="https://ai.google.dev/gemini-api/docs/text-generation" rel="noopener noreferrer"&gt;generate text output&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How to generate &lt;a href="https://ai.google.dev/gemini-api/docs/structured-output" rel="noopener noreferrer"&gt;structured output&lt;/a&gt; (JSON) with Gemini&lt;/li&gt;
&lt;li&gt;How to use &lt;a href="https://pptr.dev/" rel="noopener noreferrer"&gt;Puppeteer&lt;/a&gt; to get a page title&lt;/li&gt;
&lt;li&gt;Bonus: How to run &lt;a href="https://developer.chrome.com/docs/ai/built-in" rel="noopener noreferrer"&gt;Gemini Nano on Chrome locally&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  1: Set Up Google Gen AI SDK
&lt;/h3&gt;

&lt;p&gt;First, install the &lt;code&gt;@google/genai&lt;/code&gt; JS/TS SDK by running &lt;code&gt;npm install @google/genai&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, you'll need to get your Gemini API Key from &lt;a href="https://aistudio.google.com/apikey" rel="noopener noreferrer"&gt;https://aistudio.google.com/apikey&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's always good practice not to expose the API key on the client side; therefore, we'll run the code on the server side (Node.js).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@google/genai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GEMINI_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GEMINI_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2: Text Generation with Gemini
&lt;/h3&gt;

&lt;p&gt;Now that we've set up the Google Gen AI SDK, it's time to write the prompt. Google has an excellent guide on how to write the most efficient prompts: &lt;a href="https://ai.google.dev/gemini-api/docs/prompting-strategies" rel="noopener noreferrer"&gt;https://ai.google.dev/gemini-api/docs/prompting-strategies&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is our prompt to generate the short link:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You are a link shortener application. Your task is to generate a concise, human-readable, unique, compact, memorable short URL.&lt;br&gt;
Only use lowercase, replace spaces with hyphens, and avoid special characters. Only return the URL slug, do not include the domain.&lt;/p&gt;

&lt;p&gt;Shorten to URL for "&lt;a href="https://blog.google/products/photos/updates-ask-photos-search/" rel="noopener noreferrer"&gt;https://blog.google/products/photos/updates-ask-photos-search/&lt;/a&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We will select &lt;code&gt;gemini-2.5-flash&lt;/code&gt; as our model, and provide the contents and system instruction to Gemini by calling the &lt;code&gt;generateContent&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateShortUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;longUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Shorten to URL for "https://blog.google/products/photos/updates-ask-photos-search/"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;systemInstruction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are a link shortener application. Your task is to generate a concise, human-readable, unique, compact, memorable short URL. Only use lowercase, replace spaces with hyphens, and avoid special characters. Only return the URL slug, do not include the domain.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3. Structured Output
&lt;/h3&gt;

&lt;p&gt;By default, the SDK returns unstructured text. While this is useful for building chatbots, it's not ideal for automating the short link generation process.&lt;/p&gt;

&lt;p&gt;Fortunately, you can configure Gemini to return structured output, ensuring consistent and predictable results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateShortUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;longUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Shorten to url for "https://blog.google/products/photos/updates-ask-photos-search/"`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;systemInstruction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are an URL shortener application. Your task is to generate a concise, human-readable, unique, compact, memorable short URL.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;responseMimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;responseSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OBJECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;shortUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
              &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Only use lowercase, replace spaces with hyphens, and avoid special characters. Only return the URL slug, do not include the domain.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shortUrl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, the output will look like this: &lt;code&gt;{ "shortUrl": "google-photos-search" }&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Providing More Context with Puppeteer
&lt;/h3&gt;

&lt;p&gt;Currently, we're only providing Gemini with the page URL. Sometimes, URLs can be very abstract, like "&lt;a href="https://www.bbc.com/news/articles/cpw77qwd117o" rel="noopener noreferrer"&gt;https://www.bbc.com/news/articles/cpw77qwd117o&lt;/a&gt;".&lt;/p&gt;

&lt;p&gt;In such cases, Gemini won't be able to generate a meaningful short link unless we provide more context, such as the page title.&lt;/p&gt;

&lt;p&gt;We can use Puppeteer to scrape the page title and send it to Gemini.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getMetadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;longUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;longUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domcontentloaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateShortUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;longUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Shorten to url for "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;longUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;". The page title is &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, Gemini will return &lt;code&gt;{ "shortUrl": "ai-chatbot-google-search" }&lt;/code&gt; instead of &lt;code&gt;{ "shortUrl": "cpw77qwd117o" }&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Create an API Endpoint with Express
&lt;/h3&gt;

&lt;p&gt;Now that we have all the functions ready, we can create an Express server and expose our function via an API endpoint so it can be accessible from the frontend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generateShortUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/gemini.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getMetadata&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./src/puppeteer.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Middleware to parse JSON&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Endpoint to generate short link&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/generateShortUrl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;longUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;longUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shortUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateShortUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;longUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shortUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Start the server&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server running on http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Bonus: Run Gemini Nano on Chrome Locally
&lt;/h3&gt;

&lt;p&gt;Chrome is currently experimenting with the ability to run Gemini Nano locally. The Prompt API is currently still in the Early Preview Program (EPP) and is not production-ready at the moment. You can learn how to set it up here: &lt;a href="https://developer.chrome.com/docs/ai/get-started" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/ai/get-started&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Currently, it's nice to have the ability to run Gemini Nano locally, but make sure you have provided a cloud-based solution as a fallback.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateShortUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;longLink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;LanguageModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;initialPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are an URL shortener application. Your task is to generate a concise, human-readable, unique, compact, memorable short URL.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Shorten to url for "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;longLink&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;". The page title is &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;responseConstraint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;shortUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Only use lowercase, replace spaces with hyphens, and avoid special characters. Only return the URL slug, do not include the domain.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// { shortUrl: "example-short-url" }&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;In this article, we explored how to build a link shortener using Node.js and Express, enhancing it with AI using Google Gemini.&lt;/p&gt;

&lt;p&gt;We learned how to set up the Google Gen AI SDK, how to write a prompt to generate text output, and how to use structured output to generate JSON responses. We also learned how to use Puppeteer to extract more context, like page titles, which improves the relevance and meaningfulness of the generated short links. As a bonus, we also learned how to generate text content locally with Gemini Nano on Chrome.&lt;/p&gt;

</description>
      <category>builtwithai</category>
      <category>gemini</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI in Chrome DevTools: Apply CSS Changes Directly to Your Local Source Code with AI Assistance</title>
      <dc:creator>Henry Lim</dc:creator>
      <pubDate>Wed, 21 May 2025 18:32:23 +0000</pubDate>
      <link>https://forem.com/henrylim96/ai-in-chrome-devtools-apply-css-changes-directly-to-your-local-source-code-with-ai-assistance-1f4f</link>
      <guid>https://forem.com/henrylim96/ai-in-chrome-devtools-apply-css-changes-directly-to-your-local-source-code-with-ai-assistance-1f4f</guid>
      <description>&lt;p&gt;Chrome DevTools introduced AI Assistance in October 2024. Initially, AI assistance could only modify page content, but it couldn't apply those changes directly to your local source code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/GjvgtwSOCao?t=2440" rel="noopener noreferrer"&gt;At Google I/O 2025, a new feature was introduced&lt;/a&gt;, allowing you to apply changes from AI assistance directly to your local source code. This means any modifications made with AI assistance will now be persistent.&lt;/p&gt;

&lt;p&gt;To enable this, you need to connect your source code to Chrome DevTools via &lt;a href="https://developer.chrome.com/docs/devtools/workspaces" rel="noopener noreferrer"&gt;Workspace&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn75rtxx6r1icjxco0uas.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn75rtxx6r1icjxco0uas.png" alt="DevTools Workspace Demo" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic Workspace Folders
&lt;/h2&gt;

&lt;p&gt;Manually connecting source code can be inconvenient, especially when working on multiple projects. Fortunately, you can now automatically connect your source code to Chrome DevTools using &lt;a href="https://chromium.googlesource.com/devtools/devtools-frontend/+/main/docs/ecosystem/automatic_workspace_folders.md" rel="noopener noreferrer"&gt;Automatic Workspace Folders&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For Automatic Workspace Folders to work, you need to create a &lt;code&gt;com.chrome.devtools.json&lt;/code&gt; file inside a &lt;code&gt;.well-known/appspecific&lt;/code&gt; folder. The &lt;code&gt;com.chrome.devtools.json&lt;/code&gt; file should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "workspace": {
    "root": "/Users/foo/bar",
    "uuid": "53b029bb-c989-4dca-969b-835fecec3717"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;workspace.root&lt;/code&gt; is the absolute path to your project folder (where your source code is located).&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;workspace.uuid&lt;/code&gt; is a valid UUID, ideally a randomly generated v4 UUID, which you should generate when setting up your project folder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can generate a UUID using any online UUID generator tool, by running the command &lt;code&gt;npx --package uuid uuid v4&lt;/code&gt; in your terminal, or by calling &lt;code&gt;crypto.randomUUID()&lt;/code&gt; in JavaScript.&lt;/p&gt;

&lt;p&gt;Alternatively, you can generate the &lt;code&gt;com.chrome.devtools.json&lt;/code&gt; file using the following script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir -p .well-known/appspecific
echo "{\"workspace\":{\"root\":\"${PWD}\",\"uuid\":\"`npx --package uuid uuid v4`\"}}" &amp;gt; .well-known/appspecific/com.chrome.devtools.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are using Vite, you can install &lt;a href="https://github.com/ChromeDevTools/vite-plugin-devtools-json" rel="noopener noreferrer"&gt;Vite Plugin for DevTools Project Settings&lt;/a&gt; (&lt;code&gt;npm install -D vite-plugin-devtools-json&lt;/code&gt;). Add this plugin to your Vite configuration, and it will automatically create the &lt;code&gt;.well-known/appspecific&lt;/code&gt; folder in your Vite cache with a randomly generated UUID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {defineConfig} from 'vite';
import devtoolsJson from 'vite-plugin-devtools-json';

export default defineConfig({
  plugins: [
    devtoolsJson(),
    // ...
  ]
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  AI Assistance in Action
&lt;/h2&gt;

&lt;p&gt;Once your source code is connected to Chrome DevTools (either manually or automatically), and you've &lt;a href="https://developer.chrome.com/docs/devtools/settings/ai-innovations" rel="noopener noreferrer"&gt;confirmed AI assistance is enabled in Chrome DevTools&lt;/a&gt;, you can begin using AI assistance for persistent CSS fixes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1oyuprl71c706cgo7wod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1oyuprl71c706cgo7wod.png" alt="Screenshot of " width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's how to use it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Open Chrome DevTools.&lt;/li&gt;
&lt;li&gt; In the Elements panel, right-click the element you want to modify, and select "AI Assistance."&lt;/li&gt;
&lt;li&gt; Enter a prompt to make styling changes to your element (e.g., "Change font color to red" or "Set the image aspect ratio to letterbox").&lt;/li&gt;
&lt;li&gt; Follow the conversation to apply the changes temporarily.&lt;/li&gt;
&lt;li&gt; Click "Apply to workspace" to make the changes persistent in your local source code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fekwrasugj48bwkxhbx82.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fekwrasugj48bwkxhbx82.png" alt="Screenshot of AI Assistance with " width="800" height="644"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Getting AI to help with CSS changes right in Chrome DevTools is a good step forward for web developers. While AI can't do everything yet, but this small feature stops you from manually copying and pasting CSS from AI Assistance. This makes your work smoother and helps you get things done a bit faster. It's a handy tool to have.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>css</category>
    </item>
    <item>
      <title>Reading Xiaomi Mi Scale data with Web Bluetooth Scanning API</title>
      <dc:creator>Henry Lim</dc:creator>
      <pubDate>Sun, 10 May 2020 13:32:54 +0000</pubDate>
      <link>https://forem.com/henrylim96/reading-xiaomi-mi-scale-data-with-web-bluetooth-scanning-api-1mb9</link>
      <guid>https://forem.com/henrylim96/reading-xiaomi-mi-scale-data-with-web-bluetooth-scanning-api-1mb9</guid>
      <description>&lt;p&gt;The Web Bluetooth API provides the ability to connect and interact with Bluetooth Low Energy (BLE) peripherals. It was introduced in Chrome 56 on macOS back in January 2017.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the weight data
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This demo only works on the first generation of Mi Body Composition Scale which was released in 2017 (Model Number: XMTZC02HM).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To capture the BLE advertising packets from the Xiaomi Mi Scale, we will need to use the &lt;a href="https://googlechrome.github.io/samples/web-bluetooth/scan.html" rel="noopener noreferrer"&gt;Web Bluetooth Scanning API&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;However, the Web Bluetooth Scanning API is still under development. You need to use Chrome 79+ with the &lt;code&gt;chrome://flags/#enable-experimental-web-platform-features&lt;/code&gt; flag enabled to use the API.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;&lt;code&gt;navigator.bluetooth.requestLEScan()&lt;/code&gt; will start scanning for the advertising packets. Right before that, the permission prompt will popup asking the user for permission to access Bluetooth.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ji675h4oy6b5n79vltr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ji675h4oy6b5n79vltr.png" width="474" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the user has granted permission, we can listen to the advertising packets by using the &lt;code&gt;advertisementreceived&lt;/code&gt; event:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;advertisementreceived&lt;/code&gt; event will return information like Device Local Name, Device ID, Received Signal Strength Indicator (RSSI), Transmit power (TX Power), Service UUIDs, Manufacturer Data, and Service Data.&lt;/p&gt;

&lt;p&gt;To retrieve the payload data from Xiaomi Mi Scale, we need to get the data from &lt;code&gt;serviceData&lt;/code&gt;.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpui31rlfjznq08zwyxfc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpui31rlfjznq08zwyxfc.png" width="800" height="118"&gt;&lt;/a&gt;&lt;/p&gt;
The output result of "valueDataView.buffer"



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frf1tdxpljxnyuk2hm4g1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frf1tdxpljxnyuk2hm4g1.png" width="755" height="355"&gt;&lt;/a&gt;&lt;/p&gt;
The payload format for the Mi Scale



&lt;p&gt;Next, to get the weight data, we need to  get the value of bytes 11 and 12 (which are &lt;a href="https://hacks.mozilla.org/2017/01/typedarray-or-dataview-understanding-byte-order/" rel="noopener noreferrer"&gt;little-endian&lt;/a&gt;) then divide the value by 200.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;With that, we have successfully retrieved the weight data from Xiaomi Mi Scale using the Web Bluetooth Scanning API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extra: Reading the Impedance Data
&lt;/h2&gt;

&lt;p&gt;The Xiaomi Mi Scale is also able to measure information like muscle mass, bone mass, body fat, and more through &lt;a href="https://en.wikipedia.org/wiki/Bioelectrical_impedance_analysis" rel="noopener noreferrer"&gt;Bioelectrical Impedance Analysis&lt;/a&gt; (BIA). &lt;/p&gt;

&lt;p&gt;We can get the impedance data from bytes 9 and 10:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Next, we can convert the impedance value using &lt;a href="https://github.com/wiecosystem/Bluetooth/blob/master/sandbox/huami.health.scale2/body_metrics.py" rel="noopener noreferrer"&gt;this algorithm&lt;/a&gt;. The output should look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6xcas8wzyqnebklclx8p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6xcas8wzyqnebklclx8p.png" width="722" height="317"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7des21hm3e8e3hnj5gb6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7des21hm3e8e3hnj5gb6.gif" width="320" height="640"&gt;&lt;/a&gt;&lt;/p&gt;
Note: There's no way to get the impedance data if you are wearing socks, but I don't want to show my ugly feet… (And yes, that's a T-Rex socks)



&lt;p&gt;Demo: &lt;a href="https://scale.limhenry.xyz" rel="noopener noreferrer"&gt;https://scale.limhenry.xyz&lt;/a&gt;&lt;br&gt;
GitHub (Source Code): &lt;a href="https://github.com/limhenry/web-bluetooth-mi-scale" rel="noopener noreferrer"&gt;github.com/limhenry/web-bluetooth-mi-scale&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Web Bluetooth Documentation: &lt;a href="https://webbluetoothcg.github.io/web-bluetooth/" rel="noopener noreferrer"&gt;Link&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Web Bluetooth Scanning API  Sample: &lt;a href="https://googlechrome.github.io/samples/web-bluetooth/scan.html" rel="noopener noreferrer"&gt;Link&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Xiaomi Mi Scale BLE (Unofficial) Documentation: &lt;a href="https://github.com/wiecosystem/Bluetooth/blob/master/doc/devices/huami.health.scale2.md" rel="noopener noreferrer"&gt;Link&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;TypedArray or DataView: Understanding byte order: &lt;a href="https://hacks.mozilla.org/2017/01/typedarray-or-dataview-understanding-byte-order/" rel="noopener noreferrer"&gt;Link&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>bluetooth</category>
      <category>iot</category>
    </item>
    <item>
      <title>How to Create Malaysia Flag with HTML and CSS 🇲🇾</title>
      <dc:creator>Henry Lim</dc:creator>
      <pubDate>Fri, 30 Aug 2019 08:20:24 +0000</pubDate>
      <link>https://forem.com/henrylim96/how-to-create-malaysia-flag-with-html-and-css-3obe</link>
      <guid>https://forem.com/henrylim96/how-to-create-malaysia-flag-with-html-and-css-3obe</guid>
      <description>&lt;p&gt;Malaysia is now &lt;a href="https://en.wikipedia.org/wiki/Hari_Merdeka" rel="noopener noreferrer"&gt;62-years old&lt;/a&gt;! 🎉 Happy birthday, Malaysia! 🎂&lt;/p&gt;

&lt;p&gt;To celebrate this big day, I have decided to create the Malaysia flag using HTML and CSS (only), and without using any JavaScript, SVG, and of course, without using any images. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Um … But technically I can use emoji right? Or maybe I can use &lt;a href="https://www.npmjs.com/package/emoji-flag-my" rel="noopener noreferrer"&gt;this NPM package&lt;/a&gt;?&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Flag of Malaysia - Jalur Gemilang 🇲🇾
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhpsxoaq1gxrdpccv5n2f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhpsxoaq1gxrdpccv5n2f.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The flag of Malaysia, also known as the Malay: Jalur Gemilang ("Stripes of Glory"), is composed of a field of &lt;strong&gt;14 alternating red and white stripes&lt;/strong&gt; along the fly and a blue canton bearing &lt;strong&gt;a crescent&lt;/strong&gt; and &lt;strong&gt;a 14-point star&lt;/strong&gt; known as the Bintang Persekutuan (Federal Star).&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here are the components that I need to create using HTML and CSS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;14 Alternating Red and White Stripes:&lt;/strong&gt; Sounds easy 😎&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crescent:&lt;/strong&gt; Hmm, a big circle and a small circle inside? 🤔&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;14-Point Star:&lt;/strong&gt; I have completely no idea how to do this 🤭&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Step 1: Create Flag Layout with CSS Grid
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fql6zcqw4f3vcbggjbiqc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fql6zcqw4f3vcbggjbiqc.png" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;
(Source: &lt;a href="https://en.wikipedia.org/wiki/Flag_of_Malaysia#/media/File:Construction_sheet_of_Flag_of_Malaysia.svg" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt;)



&lt;p&gt;Firstly, I am going to create the flag layout with &lt;a href="https://css-tricks.com/snippets/css/complete-guide-grid/" rel="noopener noreferrer"&gt;CSS Grid&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;It's pretty straight-forward, I will use &lt;code&gt;grid-template-rows&lt;/code&gt; and &lt;code&gt;grid-template-columns&lt;/code&gt; properties to create the flag layout with 2 rows and 2 columns.&lt;/p&gt;

&lt;p&gt;Then, I will use &lt;code&gt;grid-column: span 2&lt;/code&gt; to expand the bottom column into a full row &lt;em&gt;(or 2 columns)&lt;/em&gt;.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
I am also using CSS Variable too




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxotbxyaj0r8btywbcwxk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxotbxyaj0r8btywbcwxk.png" width="776" height="451"&gt;&lt;/a&gt;&lt;/p&gt;
CSS Grid Layout for Jalur Gemilang






&lt;h3&gt;
  
  
  Step 2: 14 Alternating Red and White Stripes
&lt;/h3&gt;

&lt;p&gt;The &lt;em&gt;Jalur Gemilang&lt;/em&gt; consists of 7 red stripes and 7 white stripes. 8 of the stripes are located at the upper-right quarter, and the rest of the 6 stripes are located at the bottom part of the flag.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff6mxgpu88ore1ambsf70.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff6mxgpu88ore1ambsf70.png" width="776" height="451"&gt;&lt;/a&gt;&lt;/p&gt;
7 red stripes + 7 white stripes



&lt;p&gt;What I need to do now, is to create 14 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; and use CSS's &lt;code&gt;:nth-child(even)&lt;/code&gt; and &lt;code&gt;:nth-child(odd)&lt;/code&gt; rules to style the stripes. &lt;/p&gt;

&lt;p&gt;But is there any other better method to do this? 🤔&lt;/p&gt;

&lt;p&gt;Hmm … how about using &lt;code&gt;repeating-linear-gradient&lt;/code&gt;?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The &lt;code&gt;repeating-linear-gradient()&lt;/code&gt; CSS function creates an image consisting of repeating linear gradients. It is similar to &lt;code&gt;linear-gradient()&lt;/code&gt; and takes the same arguments, but it repeats the color stops infinitely in all directions so as to cover its entire container. - &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/repeating-linear-gradient" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;With that, I can use &lt;code&gt;repeating-linear-gradient&lt;/code&gt; to create the 14 red-white stripes without writing 14 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;! CSS FTW! 🎉&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 3: Crescent 🌙
&lt;/h3&gt;

&lt;p&gt;The crescent can be created using a circle with an inner shadow, which can be done using the &lt;code&gt;inset&lt;/code&gt; value. (&lt;em&gt;Thanks &lt;a href="https://www.google.com/search?q=how+to+create+crescent+with+css" rel="noopener noreferrer"&gt;Google&lt;/a&gt;!&lt;/em&gt;)&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
I found this method online ¯\_(ツ)_/¯




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgssgjh6cpx8u30yq6vcs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgssgjh6cpx8u30yq6vcs.png" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;
Here's how the flag looks like (so far)






&lt;h3&gt;
  
  
  Step 4: 14-point star
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdkrpmezogmeqt0ljroh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdkrpmezogmeqt0ljroh.png" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;That's the moment, I realized I have completely no idea how to create a 14-point star using CSS. (&lt;em&gt;And I also forgot how to draw the 14-point star by hand&lt;/em&gt; 🤦‍♀️)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrccar0ndzq0aq9ad5j1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvrccar0ndzq0aq9ad5j1.png" width="579" height="408"&gt;&lt;/a&gt;&lt;/p&gt;
Yes, it's really difficult to draw the 14-point star 🤣 (Image: &lt;a href="https://www.submerryn.com/2013/08/how-to-draw-the-star-of-our-jalur-gemilang.html" rel="noopener noreferrer"&gt;submerryn.com&lt;/a&gt;)



&lt;p&gt;So after some simple research (aka Google Search), I found 3 ways how to draw a 14-point star:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method 1:&lt;/strong&gt; Draw 7 lines, then draw a "V" between each tip.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;😀 Very easy to draw on paper&lt;/li&gt;
&lt;li&gt;😕 No idea how to recreate it with CSS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5cl6fttfg5tkwxlzw7ad.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5cl6fttfg5tkwxlzw7ad.gif" width="880" height="660"&gt;&lt;/a&gt;&lt;/p&gt;
Draw 7 lines, then draw a "V" between each tip (Image Source: &lt;a href="https://www.submerryn.com/2013/08/how-to-draw-the-star-of-our-jalur-gemilang.html" rel="noopener noreferrer"&gt;submerryn.com&lt;/a&gt;)



&lt;p&gt;&lt;strong&gt;Method 2:&lt;/strong&gt; 14 triangles surrounded by a circle.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;😀 Easy to draw on paper&lt;/li&gt;
&lt;li&gt;☹️ Hard to recreate it using CSS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fns1vs9a2y0ufy1n3r1rc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fns1vs9a2y0ufy1n3r1rc.png" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;
14 triangles surrounded by a big circle?



&lt;p&gt;&lt;strong&gt;Method 3:&lt;/strong&gt; 7 long diamond shapes with different rotation values.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🤷‍♀️ I guess no one uses this method to draw on paper?&lt;/li&gt;
&lt;li&gt;😍 Very easy to recreate it using CSS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwwnhfwrifookck1a2xkz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwwnhfwrifookck1a2xkz.gif" width="959" height="527"&gt;&lt;/a&gt;&lt;/p&gt;
7 long diamond shapes with different rotation values



&lt;p&gt;Ok, now I have found a way to create the 14-point star using CSS, the next step is to create 14 long diamond shape with different rotation values.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffaoqpv3lrytxsfgf0ubp.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffaoqpv3lrytxsfgf0ubp.gif" width="730" height="376"&gt;&lt;/a&gt;&lt;/p&gt;
Long diamond shape = 2 isosceles triangles



&lt;p&gt;The long diamond shape can be created using 2 isosceles triangles. The code should look something like this:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;The next step is to duplicate the long diamond shape for 6 more times, and add the CSS rotate function to it. As a result, the code should look like this:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;





&lt;h3&gt;
  
  
  Result 🇲🇾
&lt;/h3&gt;

&lt;p&gt;Here you have it, I have successfully created the Malaysia flag using 30 lines of HTML and ~120 lines of CSS. 🎉 &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fstb4hubj9n84m804s2lp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fstb4hubj9n84m804s2lp.png" width="800" height="549"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;a href="https://codepen.io/limhenry/pen/mdbEGYg" rel="noopener noreferrer"&gt;https://codepen.io/limhenry/pen/mdbEGYg&lt;/a&gt;



&lt;p&gt;You can look at the full code on CodePen: &lt;a href="https://codepen.io/limhenry/pen/mdbEGYg" rel="noopener noreferrer"&gt;codepen.io/limhenry/pen/mdbEGYg&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  What's next?
&lt;/h3&gt;

&lt;p&gt;However, this is not perfect yet. Few more things can be improved:&lt;/p&gt;

&lt;h4&gt;
  
  
  😤 Issue #1: Sub-pixel Rendering
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ntknjc77a54r2p32knw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ntknjc77a54r2p32knw.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
Sub-pixel rendering issue on Firefox



&lt;p&gt;If you open the &lt;a href="https://codepen.io/limhenry/pen/mdbEGYg" rel="noopener noreferrer"&gt;CodePen link&lt;/a&gt; in Firefox, you might notice the sub-pixel rendering issue (&lt;em&gt;The weird line in the 14-point star&lt;/em&gt;). &lt;/p&gt;

&lt;p&gt;However, it's actually possible to fix it using &lt;code&gt;transform: translateZ(1px);&lt;/code&gt; (&lt;em&gt;&lt;a href="https://codepen.io/limhenry/pen/mdbEGYg" rel="noopener noreferrer"&gt;Refer to line 111&lt;/a&gt;&lt;/em&gt;). By doing this, the browser will render it using hardware acceleration or GPU acceleration, instead of the browser's slower software rendering engine.&lt;/p&gt;

&lt;h4&gt;
  
  
  😅 Issue #2: Flag is Not Responsive
&lt;/h4&gt;

&lt;p&gt;This can be done with a few lines of JavaScript code, &lt;em&gt;but maybe I can write another article just for this?&lt;/em&gt; 🤔&lt;/p&gt;




&lt;p&gt;Anyway, it's time for me to go eat some &lt;a href="https://en.wikipedia.org/wiki/Nasi_lemak" rel="noopener noreferrer"&gt;Nasi Lemak&lt;/a&gt; 😋 Happy Birthday Malaysia! 🎉&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3tlqafnvp33nyei5k5yw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3tlqafnvp33nyei5k5yw.jpg" width="800" height="491"&gt;&lt;/a&gt;&lt;/p&gt;
HAPPY BIRTHDAY, MALAYSIA! 🇲🇾🎂 (Image: &lt;a href="https://www.instagram.com/ngmykia/" rel="noopener noreferrer"&gt;NG-MY KIA&lt;/a&gt; / 📸 Myself)



</description>
      <category>webdev</category>
      <category>html</category>
      <category>css</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How to Add Internationalization (i18n) to your Preact application</title>
      <dc:creator>Henry Lim</dc:creator>
      <pubDate>Tue, 18 Jun 2019 14:45:26 +0000</pubDate>
      <link>https://forem.com/henrylim96/how-to-add-internationalization-i18n-to-your-preact-application-5gd6</link>
      <guid>https://forem.com/henrylim96/how-to-add-internationalization-i18n-to-your-preact-application-5gd6</guid>
      <description>&lt;p&gt;🇹🇼 中文版 (Chinese Version): &lt;a href="https://dev.to/henrylim96/i18n-preact-3pie"&gt;https://dev.to/henrylim96/i18n-preact-3pie&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Internationalization (i18n)?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Internationalization&lt;/strong&gt; is the design and development of a product, application or document content that enables easy localization for target audiences that vary in culture, region, or language.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, you are going to use the &lt;code&gt;preact-i18n&lt;/code&gt; library to add internationalization to your Preact application.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Setup Preact CLI &amp;amp; Create new project
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Side Note: If you are already familiar with Preact, you may skip to the next step.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you haven't installed the Preact CLI on your machine, use the following command to install the CLI. Make sure you have Node.js 6.x or above installed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ npm install -g preact-cli&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Once the Preact CLI is installed, let's create a new project using the &lt;code&gt;default&lt;/code&gt; template, and call it &lt;code&gt;my-project&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ preact create default my-project&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Start the development server with the command below:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ cd my-project &amp;amp;&amp;amp; npm run start&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now, open your browser and go to &lt;code&gt;http://localhost:8080&lt;/code&gt;, and you should see something like this on your screen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq4342dguc8t7btgqoilu.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq4342dguc8t7btgqoilu.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add preact-i18n library
&lt;/h2&gt;

&lt;p&gt;Install the &lt;code&gt;preact-i18n&lt;/code&gt; library to your project using the command below:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ npm install --save preact-i18n&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;preact-i18n&lt;/code&gt; is very easy to use, and most importantly, it's extremely small, around 1.3kb after gzipped. You can learn more about the library here: &lt;a href="https://github.com/synacor/preact-i18n" rel="noopener noreferrer"&gt;https://github.com/synacor/preact-i18n&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Create a definition file
&lt;/h2&gt;

&lt;p&gt;Once you have the library installed, you will need to create a definition file, which you will store all the translate strings in a JSON file. &lt;/p&gt;

&lt;p&gt;In this case, you will need to save this file in &lt;code&gt;src/i18n/zh-tw.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"主頁"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"這是個Home組件。"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 4: Import IntlProvider and definition file
&lt;/h2&gt;

&lt;p&gt;Next, open the &lt;code&gt;app.js&lt;/code&gt; file, which is located in the &lt;code&gt;src/components&lt;/code&gt; folder. Then, import the &lt;code&gt;IntlProvider&lt;/code&gt; and your &lt;code&gt;definition&lt;/code&gt; file to the &lt;code&gt;app.js&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IntlProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../i18n/zh-tw.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 5: Expose the definition via IntlProvider
&lt;/h2&gt;

&lt;p&gt;After that, you will need to expose the definition file to the whole app via &lt;code&gt;&amp;lt;IntlProvider&amp;gt;&lt;/code&gt;. By doing this, you will be able to read the definition file everywhere in the app.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IntlProvider&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;definition&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;IntlProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;At this moment, here's how your &lt;code&gt;app.js&lt;/code&gt; file should looks like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Router&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Header&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../routes/home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../routes/profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Import IntlProvider and the definition file.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IntlProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../i18n/zh-tw.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

 &lt;span class="nx"&gt;handleRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="c1"&gt;// Expose the definition to your whole app via &amp;lt;IntlProvider&amp;gt;&lt;/span&gt;
   &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IntlProvider&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;definition&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
     &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
     &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Router&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleRoute&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Home&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Profile&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/profile/"&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"me"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Profile&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/profile/:user"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
     &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
   &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;IntlProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 6: Use Text to translate string literals
&lt;/h2&gt;

&lt;p&gt;You are almost done, now you just need to replace the text in the page with &lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt;. In this case, you will need to update the content of the home page (&lt;code&gt;src/routes/home/index.js&lt;/code&gt;) by adding the &lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt; inside the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; tags.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"home.title"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Home&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"home.text"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;This is the Home component.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59fj7ql15ggypf9gtkay.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59fj7ql15ggypf9gtkay.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Fallback Text
&lt;/h2&gt;

&lt;p&gt;In order to prevent blank text being rendered in the page, you should set a fallback text to the &lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt;. If you didn't include the definition for &lt;code&gt;unknown.definition&lt;/code&gt;, the library will render any text contained within &lt;code&gt;&amp;lt;Text&amp;gt;…&amp;lt;/Text&amp;gt;&lt;/code&gt; as fallback text:&lt;br&gt;
&lt;br&gt;
 &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Text&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"unknown.definition"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;This is a fallback text.&lt;span class="nt"&gt;&amp;lt;/Text&amp;gt;&lt;/span&gt;
// It will render this text: "This is a fallback text."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Localizer and MarkupText
&lt;/h2&gt;

&lt;p&gt;If you want to translate the text of the HTML attribute's value (ie: &lt;code&gt;placeholder=""&lt;/code&gt;, &lt;code&gt;title=""&lt;/code&gt;, etc …), then you will need to use &lt;code&gt;&amp;lt;Localizer&amp;gt;&lt;/code&gt; instead of &lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, if you want to include HTML markup in your rendered string, then you will need to use &lt;code&gt;&amp;lt;MarkupText&amp;gt;&lt;/code&gt;. With this component, your text will be rendered in a &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;In the example below, you are going to add few more lines of code to your definition file. &lt;code&gt;first_name&lt;/code&gt; and &lt;code&gt;last_name&lt;/code&gt; will be used for the &lt;code&gt;&amp;lt;Localizer&amp;gt;&lt;/code&gt;'s example, and &lt;code&gt;link&lt;/code&gt; for the example for &lt;code&gt;&amp;lt;MarkupText&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"first_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"名"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"姓"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"這是個&amp;lt;a href='https://www.google.com'&amp;gt;連結&amp;lt;/a&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;With this, you will able to use &lt;code&gt;&amp;lt;Localizer&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;MarkupText&amp;gt;&lt;/code&gt; in the page. Please take note that you need to import &lt;code&gt;Localizer&lt;/code&gt; and &lt;code&gt;MarkupText&lt;/code&gt; to the &lt;code&gt;src/routes/home/index.js&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Localizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MarkupText&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Localizer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"first_name"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Localizer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Localizer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"last_name"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Localizer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MarkupText&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      This is a &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://www.google.com"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;link&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;MarkupText&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzmhhjjef49rwdojwlhk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzmhhjjef49rwdojwlhk.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Templating
&lt;/h2&gt;

&lt;p&gt;If you want to inject a custom string or value into the definition, you could do it with the &lt;code&gt;fields&lt;/code&gt; props.&lt;/p&gt;

&lt;p&gt;First, you will need to update the definition file with the &lt;code&gt;{{field}}&lt;/code&gt; placeholder. The placeholder will get replaced with the matched keys in an object you passed in the &lt;code&gt;fields&lt;/code&gt; props.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{count}} / {{total}} 頁"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Next, you will need to add the &lt;code&gt;fields&lt;/code&gt; attribute together with the value into the &lt;code&gt;&amp;lt;Text /&amp;gt;&lt;/code&gt;. As a result, your code should looks like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"page"&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
         5 / 10 Pages
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwcuwer9gt39w4xwb84b.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwcuwer9gt39w4xwb84b.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Pluralization
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;preact-i18n&lt;/code&gt;, you have 3 ways to specific the pluralization values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"key": { "singular":"apple", "plural":"apples" }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"key": { "none":"no apples", "one":"apple", "many":"apples" }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"key": ["apples", "apple"]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the next example, you will combine both pluralization and templating. First, you will need to update the definition file with the code below:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"singular"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Henry has {{count}} apple."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"plural"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Henry has {{count}} apples."&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Next, you will update the home page (&lt;code&gt;src/routes/home/index.js&lt;/code&gt;) with the following code:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"apple"&lt;/span&gt; &lt;span class="na"&gt;plural&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"apple"&lt;/span&gt; &lt;span class="na"&gt;plural&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxfdv9a211m3fons1t0s.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxfdv9a211m3fons1t0s.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the method above, you will able to add pluralization and templating to your Preact application.&lt;/p&gt;


&lt;h2&gt;
  
  
  Dynamically import language definition file
&lt;/h2&gt;

&lt;p&gt;In a real-world scenario, you would like to set the language site based on the user's choice, which is either based on the &lt;code&gt;navigator.language&lt;/code&gt; or the user can change the site language on their own.&lt;/p&gt;

&lt;p&gt;However, in order to prevent you from importing all the unnecessary definition files to the project, you can import the language definition file dynamically by using &lt;code&gt;import()&lt;/code&gt;. By doing this, you can import the language definition file based on the user's choice.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IntlProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;defaultDefinition&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../i18n/zh-tw.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;defaultDefinition&lt;/span&gt; 
  &lt;span class="p"&gt;}&lt;/span&gt; 
  &lt;span class="nx"&gt;changeLanguage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="c1"&gt;// Call this function to change language &lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`../i18n/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt; 
  &lt;span class="p"&gt;};&lt;/span&gt; 
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IntlProvider&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;definition&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;IntlProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;);&lt;/span&gt; 
  &lt;span class="p"&gt;}&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In this case, you can call the &lt;code&gt;this.changeLanguage('zh-TW')&lt;/code&gt; function to change the site language.&lt;/p&gt;


&lt;h2&gt;
  
  
  Who's using preact-i18n?
&lt;/h2&gt;

&lt;p&gt;I am using &lt;code&gt;preact-i18n&lt;/code&gt; for my side project: Remote for Slides.&lt;/p&gt;

&lt;p&gt;Remote for Slides is a Progressive Web App + Chrome Extension that allows the user to control their Google Slides on any device, remotely, without the need of any extra hardware.&lt;/p&gt;

&lt;p&gt;Remote for Slides Progressive Web App supports more than 8 languages, which includes: Català, English, Español, Euskera, Français, Polski, Traditional Chinese, and Simplified Chinese.&lt;/p&gt;

&lt;p&gt;In this side project, I am using the "dynamically import language definition file" method I mentioned earlier. This could prevent the web app from loading some unnecessary definition language files, thus this will improve the page performance.&lt;/p&gt;

&lt;p&gt;Furthermore, the Remote for Slides Progressive Web App will set the language based on the browser's language (&lt;code&gt;navigator.language&lt;/code&gt;), or based on the URL parameter (ie: &lt;a href="https://slides.limhenry.xyz/?hl=zh-tw" rel="noopener noreferrer"&gt;s.limhenry.xyz/?hl=zh-tw&lt;/a&gt;), or the user can change it from the &lt;a href="https://slides.limhenry.xyz/settings/language" rel="noopener noreferrer"&gt;Settings page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5qohg91c2nc2je7ve0g2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5qohg91c2nc2je7ve0g2.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can learn more about Remote for Slides here:&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/henrylim96" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4625%2F7caac3f9-4594-4e71-a79b-0fa8a10b6e6c.jpg" alt="henrylim96"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/henrylim96/meet-remote-for-slides-a-new-way-to-control-your-presentation-slides-1o7n" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Meet Remote for Slides, a new way to control your presentation slides&lt;/h2&gt;
      &lt;h3&gt;Henry Lim ・ Apr 2 '19&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#pwa&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#web&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#android&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#googleslides&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/synacor/preact-i18n" rel="noopener noreferrer"&gt;preact-i18n&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/preactjs/preact-cli" rel="noopener noreferrer"&gt;Preact CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://preactjs.com/" rel="noopener noreferrer"&gt;Preact&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>preact</category>
      <category>web</category>
      <category>javascript</category>
      <category>internationalization</category>
    </item>
    <item>
      <title>如何國際化(i18n)您的Preact項目</title>
      <dc:creator>Henry Lim</dc:creator>
      <pubDate>Tue, 18 Jun 2019 14:45:16 +0000</pubDate>
      <link>https://forem.com/henrylim96/i18n-preact-3pie</link>
      <guid>https://forem.com/henrylim96/i18n-preact-3pie</guid>
      <description>&lt;p&gt;🇺🇸 English Version (英文版): &lt;a href="https://dev.to/henrylim96/how-to-add-internationalization-i18n-to-your-preact-application-5gd6"&gt;https://dev.to/henrylim96/how-to-add-internationalization-i18n-to-your-preact-application-5gd6&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  什麼是國際化 (i18n)？
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;國際化 (Internationalization)，也被稱為i18n，意思指i和n之间有18个字母。國際化是指修改軟體使之能適應目標市場的語言、地區差異以及技術需要。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在這篇文章中，我們將會使用&lt;a href="https://github.com/synacor/preact-i18n" rel="noopener noreferrer"&gt;preact-i18n&lt;/a&gt;來國際化您的Preact項目。&lt;/p&gt;




&lt;h2&gt;
  
  
  步驟 1：設置Preact CLI, 並創建一個新的項目
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;注: 如果您已經熟悉Preact了，您可以跳到下一步。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;如果您還沒有將Preact CLI安裝到您的電腦，請使用以下的命令來安裝Preact CLI。這CLI需要Node.js 版本 6.x 或以上。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ npm install -g preact-cli&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;當您已經成功將Preact CLI安裝到您的電腦中，我們將會使用以下的命令來創建一個名為&lt;code&gt;my-project&lt;/code&gt;的項目。在這個項目中，我們將會使用&lt;code&gt;default&lt;/code&gt;模板。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ preact create default my-project&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;之後呢，您可以使用以下的命令來啟動本地測試服務器。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ cd my-project &amp;amp;&amp;amp; npm run start&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;這個時候，我們需要打開我們的遊覽器，並前往&lt;code&gt;http://localhost:8080&lt;/code&gt;, 你將會看到像這樣類似的畫面：&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq4342dguc8t7btgqoilu.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq4342dguc8t7btgqoilu.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  步驟 2：安裝preact-18n
&lt;/h2&gt;

&lt;p&gt;我們將會使用以下的命令來安裝&lt;code&gt;preact-i18n&lt;/code&gt;到您的項目中。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ npm install --save preact-i18n&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;preact-i18n&lt;/code&gt;是非常容易使用的。更重要的是， 這&lt;code&gt;preact-i18n&lt;/code&gt;在gzip之後才佔據不到1.3kb的大小。&lt;/p&gt;

&lt;h2&gt;
  
  
  步驟 3：創建definition文件
&lt;/h2&gt;

&lt;p&gt;當你已將&lt;code&gt;preact-i18n&lt;/code&gt;安裝到您的項目之後，我們將會創建一個definition文件。我們將會把我們要翻譯的文字和句子，儲存在這個JSON文件中。&lt;/p&gt;

&lt;p&gt;我們將會把這個definition文件儲存在&lt;code&gt;src/i18n/zh-tw.json&lt;/code&gt;。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"home"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"主頁"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"這是個Home組件。"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  步驟 4：導入IntlProvider及definition文件
&lt;/h2&gt;

&lt;p&gt;接下來，我們將會從&lt;code&gt;src/components&lt;/code&gt;中打開&lt;code&gt;app.js&lt;/code&gt;。我們將會在這個文件中導入&lt;code&gt;IntlProvider&lt;/code&gt;及&lt;code&gt;definition&lt;/code&gt;文件。&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IntlProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../i18n/zh-tw.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  步驟 5：把IntlProvider放在項目中最高層級的組件
&lt;/h2&gt;

&lt;p&gt;然後呢，我們將會在把&lt;code&gt;&amp;lt;IntlProvider&amp;gt;&lt;/code&gt;放在項目中最高層級的組件，也就是我們的&lt;code&gt;app.js&lt;/code&gt;。這樣子，我們就能在這Preact項目中的任何一個組件中讀取到&lt;code&gt;definition&lt;/code&gt;文件。&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IntlProvider&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;definition&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;IntlProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;在這個時候，您的&lt;code&gt;app.js&lt;/code&gt;文件的內容應該是要跟以下的例子類似:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Router&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Header&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../routes/home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Profile&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../routes/profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// 導入 IntlProvider 及 definition 文件。&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IntlProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../i18n/zh-tw.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

 &lt;span class="nx"&gt;handleRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="c1"&gt;// 把 &amp;lt;IntlProvider&amp;gt; 放在項目中最高層級的組件&lt;/span&gt;
   &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IntlProvider&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;definition&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
     &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
     &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Router&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleRoute&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Home&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Profile&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/profile/"&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"me"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Profile&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/profile/:user"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
     &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
   &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;IntlProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  步驟 6：使用Text來顯示翻譯字符串文字
&lt;/h2&gt;

&lt;p&gt;我們只差一步就成功了。在以下的例子中，我們將會翻譯主頁(&lt;code&gt;src/routes/home/index.js&lt;/code&gt;)中所有的文字。現在，我們只需要把網頁中的字改成&lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt;。因此，我們將會把&lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt;添加進&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;和&lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;裡。&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"home.title"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Home&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"home.text"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;This is the Home component.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59fj7ql15ggypf9gtkay.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59fj7ql15ggypf9gtkay.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  後備文字
&lt;/h2&gt;

&lt;p&gt;為了避免網頁中出現空白，我們應該在&lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt;中輸入後備文字。 如果&lt;code&gt;preact-i18n&lt;/code&gt;無法在您的&lt;code&gt;definition&lt;/code&gt;中找到相關的文字或句子，那&lt;code&gt;preact-i18n&lt;/code&gt;將會使用你剛才在 &lt;code&gt;&amp;lt;Text&amp;gt;…&amp;lt;/Text&amp;gt;&lt;/code&gt;輸入的後備文字。&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Text&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"unknown.definition"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;This is a fallback text.&lt;span class="nt"&gt;&amp;lt;/Text&amp;gt;&lt;/span&gt;
// 這將會渲染: "This is a fallback text."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Localizer 和 MarkupText
&lt;/h2&gt;

&lt;p&gt;如果您是想要翻譯HTML屬性中的文字 (比如說 &lt;code&gt;placeholder=""&lt;/code&gt;或是&lt;code&gt;title=""&lt;/code&gt;等等)，您應該使用&lt;code&gt;&amp;lt;Localizer&amp;gt;&lt;/code&gt;，而並不是使用&lt;code&gt;&amp;lt;Text&amp;gt;&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;相反的，如果您是想要在您的翻譯的文字或句子中使用HTML Markup, 您必須使用&lt;code&gt;&amp;lt;MarkupText&amp;gt;&lt;/code&gt;。&lt;code&gt;&amp;lt;MarkupText&amp;gt;&lt;/code&gt;將會把已翻譯好的文字或句子渲染在一個&lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt;tag中。&lt;/p&gt;

&lt;p&gt;在以下的例子中，我們將會在我們的&lt;code&gt;definition&lt;/code&gt;文件中添加多幾行的代碼。&lt;code&gt;first_name&lt;/code&gt;及&lt;code&gt;last_name&lt;/code&gt;，將會使用在&lt;code&gt;&amp;lt;Localizer&amp;gt;&lt;/code&gt;中的例子。 而我們會在&lt;code&gt;&amp;lt;MarkupText&amp;gt;&lt;/code&gt;中的例子使用&lt;code&gt;link&lt;/code&gt;。&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"first_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"名"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"姓"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"這是個&amp;lt;a href='https://www.google.com'&amp;gt;連結&amp;lt;/a&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;在你更新主頁(&lt;code&gt;src/routes/home/index.js&lt;/code&gt;)中的內容之前，記得將&lt;code&gt;Localizer&lt;/code&gt;和&lt;code&gt;MarkupText&lt;/code&gt;導入到該頁中：&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Localizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MarkupText&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Localizer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"first_name"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Localizer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Localizer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"last_name"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Localizer&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MarkupText&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      This is a &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://www.google.com"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;link&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;MarkupText&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzmhhjjef49rwdojwlhk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzmhhjjef49rwdojwlhk.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  模板 (Templating)
&lt;/h2&gt;

&lt;p&gt;如果您想要在您的definition中注入一些自定義的字符串，您可以使用&lt;code&gt;fields&lt;/code&gt;屬性來實現。&lt;/p&gt;

&lt;p&gt;首先呢，我們需要先更新我們的definition文件。在我們的definition文件中，我們需要將我們要被自定義的字符串替代的文字，更改成像&lt;code&gt;{{count}}&lt;/code&gt;或者是&lt;code&gt;{{total}}&lt;/code&gt;這樣子的佔位符。&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{count}} / {{total}} 頁"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;之後呢，我們需要在我們的&lt;code&gt;&amp;lt;Text /&amp;gt;&lt;/code&gt;中加入&lt;code&gt;fields&lt;/code&gt;屬性。因此，您的代碼應如下所示：&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"page"&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
         5 / 10 Pages
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwcuwer9gt39w4xwb84b.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwcuwer9gt39w4xwb84b.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  复数 (Pluralization)
&lt;/h2&gt;

&lt;p&gt;如果您要翻譯的語言有复数的話(比如說像英文：apple / apples)，您可以使用以下其中一個方法，來把已翻譯好的文字和句子放進您的definition文件裡。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"key": { "singular":"apple", "plural":"apples" }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"key": { "none":"no apples", "one":"apple", "many":"apples" }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"key": ["apples", "apple"]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在以下的例子中，我們將會把模板和复数的例子結合在一起。但在那之前，我們需要更新我們的definition文件：&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"singular"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Henry has {{count}} apple."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="nl"&gt;"plural"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Henry has {{count}} apples."&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;接著，我們將會把以下的代碼粘貼到&lt;code&gt;src/routes/home/index.js&lt;/code&gt;中：&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"apple"&lt;/span&gt; &lt;span class="na"&gt;plural&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"apple"&lt;/span&gt; &lt;span class="na"&gt;plural&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Home&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxfdv9a211m3fons1t0s.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxfdv9a211m3fons1t0s.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;根據以上的步驟，你就能在您的Preact項目中使用模板和复数。&lt;/p&gt;


&lt;h2&gt;
  
  
  動態導入definition文件
&lt;/h2&gt;

&lt;p&gt;在現實情況中，您將會根據用戶的選擇來設定網頁的語言。&lt;/p&gt;

&lt;p&gt;您可以使用遊覽器的語言(通過&lt;code&gt;navigator.language&lt;/code&gt;)， 或者是讓用戶自己手動更換語言。&lt;/p&gt;

&lt;p&gt;然而，為了避免我們將不必要的definition文件導入進去，我們可以使用&lt;code&gt;import()&lt;/code&gt;來實現動態導入definition文件。這樣一來，我們只會導入用戶所選擇的語言所需要的definition文件。&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IntlProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;defaultDefinition&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../i18n/zh-tw.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;defaultDefinition&lt;/span&gt; 
  &lt;span class="p"&gt;}&lt;/span&gt; 
  &lt;span class="nx"&gt;changeLanguage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="c1"&gt;// 我們可以使用這個函數來更換語言&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`../i18n/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt; 
  &lt;span class="p"&gt;};&lt;/span&gt; 
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IntlProvider&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;definition&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; 
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;IntlProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;);&lt;/span&gt; 
  &lt;span class="p"&gt;}&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;根據以上的例子，我們可以使用這函數:&lt;code&gt;this.changeLanguage("zh-TW")&lt;/code&gt; 來導入definition文件並更改網頁的語言。&lt;/p&gt;


&lt;h2&gt;
  
  
  誰在使用preact-i18n？
&lt;/h2&gt;

&lt;p&gt;我自己的業餘項目: Remote for Slides，正在使用著&lt;code&gt;preact-i18n&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;Remote for Slides是一個漸進式網絡應用程序(PWA) + Chrome 擴充器。這能讓用戶在任何設備上，遠程遙控Google簡報。是時候跟昂貴的翻頁筆說再見了。&lt;/p&gt;

&lt;p&gt;Remote for Slides 漸進式網絡應用程序支持多達8個語言，包括了英文、繁體中文、簡體中文、加泰羅尼亞文、西班牙文、 法文、波蘭文、以及Euskera。&lt;/p&gt;

&lt;p&gt;在這個項目中，我也使用了我在剛才提到的 "動態導入definition文件" 的方法。這可以避免應用程序導入一些沒使用到的definition文件。這將會提升應用程序性能。&lt;/p&gt;

&lt;p&gt;除此之外，Remote for Slides 漸進式網絡應用程序也將會自動地設置語言。這應用程序將會根據遊覽器的語言(&lt;code&gt;navigator.language&lt;/code&gt;)、或者是根據URL中的parameter (ie: &lt;a href="https://slides.limhenry.xyz/?hl=zh-tw" rel="noopener noreferrer"&gt;s.limhenry.xyz/?hl=zh-tw&lt;/a&gt;)來更改語言。 當然，用戶也可以從&lt;a href="https://slides.limhenry.xyz/settings/language" rel="noopener noreferrer"&gt;設置&lt;/a&gt;中更改語言。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5qohg91c2nc2je7ve0g2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5qohg91c2nc2je7ve0g2.jpg" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;您可以在這裡知道關於Remote for Slides更多的訊息：&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/henrylim96" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4625%2F7caac3f9-4594-4e71-a79b-0fa8a10b6e6c.jpg" alt="henrylim96"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/henrylim96/meet-remote-for-slides-a-new-way-to-control-your-presentation-slides-1o7n" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Meet Remote for Slides, a new way to control your presentation slides&lt;/h2&gt;
      &lt;h3&gt;Henry Lim ・ Apr 2 '19&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#pwa&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#web&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#android&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#googleslides&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;






&lt;h2&gt;
  
  
  更多資源
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/synacor/preact-i18n" rel="noopener noreferrer"&gt;preact-i18n&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/preactjs/preact-cli" rel="noopener noreferrer"&gt;Preact CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://preactjs.com/" rel="noopener noreferrer"&gt;Preact&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>preact</category>
      <category>web</category>
      <category>javascript</category>
      <category>internationalization</category>
    </item>
    <item>
      <title>Adding Splash Screen to Trusted Web Activity with Android’s FileProvider</title>
      <dc:creator>Henry Lim</dc:creator>
      <pubDate>Fri, 03 May 2019 15:08:14 +0000</pubDate>
      <link>https://forem.com/henrylim96/adding-splash-screen-to-trusted-web-activity-48p3</link>
      <guid>https://forem.com/henrylim96/adding-splash-screen-to-trusted-web-activity-48p3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ THIS ARTICLE IS MORE THAN 1 YEAR OLD.&lt;br&gt;
Please refer to &lt;a href="https://developer.chrome.com/docs/android/trusted-web-activity/integration-guide/#making-a-splash" rel="noopener noreferrer"&gt;this article&lt;/a&gt; on how to add splash screen to Trusted Web Activities.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Learn how you can add a splash screen to your Trusted Web Activity (TWA) with Android’s FileProvider.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Trusted Web Activity?
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Trusted Web Activities (TWA) are a new way to integrate your web-app content such as your PWA with your Android app using a protocol based on Custom Tabs.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;⚠️ &lt;strong&gt;This method is NOT production ready, and it only works on Chrome 75+. You have been warned!️&lt;/strong&gt; ️️⚠️&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Add Logo
&lt;/h2&gt;

&lt;p&gt;Add your app logo to the drawable folder (using Asset Studio). In this case, I set the logo size to 96dp and named it &lt;code&gt;ic_splash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk237u1ziuhbn7so96c2v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk237u1ziuhbn7so96c2v.png" width="800" height="624"&gt;&lt;/a&gt;&lt;/p&gt;
drawable/ic_splash



&lt;h2&gt;
  
  
  Step 2: Create an XML file
&lt;/h2&gt;

&lt;p&gt;Create a new XML file in the XML resources folder. In this case, I’m calling it &lt;code&gt;file_path.xml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fis5nrldurj66mfh7lvgu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fis5nrldurj66mfh7lvgu.png" alt="Adding " width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;
Adding "file_path.xml"



&lt;p&gt;Then, add the following code to the XML file you just created:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;h2&gt;
  
  
  Step 3: Update AndroidManifest.xml
&lt;/h2&gt;

&lt;p&gt;Update your &lt;code&gt;AndroidManifest.xml&lt;/code&gt; file by adding a new &lt;code&gt;&amp;lt;meta-data&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;provider&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg52c73qqdtmircbph39s.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg52c73qqdtmircbph39s.jpg" alt="AndroidManifest.xml" width="696" height="881"&gt;&lt;/a&gt;&lt;/p&gt;
AndroidManifest.xml



&lt;h2&gt;
  
  
  Step 4: Update the Trusted Web Activity Library
&lt;/h2&gt;

&lt;p&gt;Make sure you are using the latest build of Chrome Tabs Client (or at least build 3679335).&lt;/p&gt;

&lt;p&gt;Check the latest build here: &lt;a href="https://chromium.googlesource.com/custom-tabs-client/+log" rel="noopener noreferrer"&gt;https://chromium.googlesource.com/custom-tabs-client/+log&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can update the library in the &lt;code&gt;build.gradle&lt;/code&gt; file:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9qhn2nvqb9c3496ghriy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9qhn2nvqb9c3496ghriy.png" width="800" height="108"&gt;&lt;/a&gt;&lt;/p&gt;
build.gradle



&lt;h2&gt;
  
  
  Step 5: Party Time 🎉
&lt;/h2&gt;

&lt;p&gt;Now you should see your app logo showing in the splash screen. Yay!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3sz1d9oblzxfborya9f.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3sz1d9oblzxfborya9f.jpeg" alt="Remote for Slides Lite Splash Screen" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;
Remote for Slides Lite Splash Screen






&lt;p&gt;CSS Tricks has an awesome article about how you can implement Trusted Web Activity to your Progressive Web App, you can learn more here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://css-tricks.com/how-to-get-a-progressive-web-app-into-the-google-play-store/" rel="noopener noreferrer"&gt;https://css-tricks.com/how-to-get-a-progressive-web-app-into-the-google-play-store/&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This method is based on the doc from the custom-tabs-client library:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chromium.googlesource.com/custom-tabs-client/+/refs/heads/master/customtabs/src/android/support/customtabs/trusted/LauncherActivity.java" rel="noopener noreferrer"&gt;https://chromium.googlesource.com/custom-tabs-client/+/refs/heads/master/customtabs/src/android/support/customtabs/trusted/LauncherActivity.java&lt;/a&gt;&lt;/p&gt;

</description>
      <category>web</category>
      <category>pwa</category>
      <category>twa</category>
      <category>android</category>
    </item>
    <item>
      <title>Meet Remote for Slides, a new way to control your presentation slides</title>
      <dc:creator>Henry Lim</dc:creator>
      <pubDate>Tue, 02 Apr 2019 15:18:11 +0000</pubDate>
      <link>https://forem.com/henrylim96/meet-remote-for-slides-a-new-way-to-control-your-presentation-slides-1o7n</link>
      <guid>https://forem.com/henrylim96/meet-remote-for-slides-a-new-way-to-control-your-presentation-slides-1o7n</guid>
      <description>&lt;p&gt;Say hello to &lt;strong&gt;Remote for Slides&lt;/strong&gt;. Remote for Slides allows you to control Google Slides on any device, remotely, without the need of any extra hardware.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started with Remote for Slides
&lt;/h2&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/LLCXFAd94pk"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install Remote for Slides Chrome Extension from the &lt;a href="https://chrome.google.com/webstore/detail/remote-for-google-slides/pojijacppbhikhkmegdoechbfiiibppi?utm=medium" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Open your Google Slides in Editor Mode, then click on the "Present with Remote" button.&lt;/li&gt;
&lt;li&gt;Next, click on the "Start Remote" button to show the 6-digit code.&lt;/li&gt;
&lt;li&gt;Enter the 6-digit code in the &lt;a href="http://s.limhenry.xyz/" rel="noopener noreferrer"&gt;Remote for Slides Progressive Web App&lt;/a&gt; or in the &lt;a href="https://play.google.com/store/apps/details?id=xyz.limhenry.slides.lite" rel="noopener noreferrer"&gt;Remote for Slides Lite Android App&lt;/a&gt;, and you are ready to go!&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What’s new in Remote for Slides
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5e2n70qeumzqp7f1gbdv.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5e2n70qeumzqp7f1gbdv.jpeg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✅ Control presentation slide (Next/Previous slide)&lt;br&gt;
✅ View speaker notes with adjustable font size&lt;br&gt;
✅️ View timer&lt;br&gt;
✅ Dark mode&lt;br&gt;
✅ Black mode (Works best with OLED screen)&lt;br&gt;
✅ Multiple languages support&lt;/p&gt;

&lt;h3&gt;
  
  
  Speaker notes &amp;amp; Timer
&lt;/h3&gt;

&lt;p&gt;You can now view your speaker notes directly in the &lt;a href="https://slides.limhenry.xyz/" rel="noopener noreferrer"&gt;Remote for Slides Progressive Web App&lt;/a&gt; or in the &lt;a href="https://play.google.com/store/apps/details?id=xyz.limhenry.slides.lite" rel="noopener noreferrer"&gt;Remote for Slides Lite Android App&lt;/a&gt;. To make sure you can keep track of time, we also added a timer at the bottom of the screen. We want you to have a successful presentation!&lt;/p&gt;

&lt;h3&gt;
  
  
  Dark Mode &amp;amp; Black Mode
&lt;/h3&gt;

&lt;p&gt;Are you going to giving a presentation in a dark environment? You can now enable the &lt;a href="https://www.youtube.com/watch?v=QZNG4Lp7dGs" rel="noopener noreferrer"&gt;dark mode&lt;/a&gt; from the settings page. And if you are using a device with an OLED display, we recommend you to enable the &lt;a href="https://twitter.com/remoteforslides/status/1112581284830666752" rel="noopener noreferrer"&gt;black mode&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple languages support
&lt;/h3&gt;

&lt;p&gt;&lt;a href="http://slides.limhenry.xyz/" rel="noopener noreferrer"&gt;Remote for Slides Progressive Web App&lt;/a&gt; and &lt;a href="https://play.google.com/store/apps/details?id=xyz.limhenry.slides.lite" rel="noopener noreferrer"&gt;Remote for Slides Lite Android App&lt;/a&gt; are now available in 8 languages: Català, English, Español, Euskera, Français, Polski, Simplified Chinese, and Traditional Chinese.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0nbu1wazfzonibn590u2.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0nbu1wazfzonibn590u2.jpeg"&gt;&lt;/a&gt;&lt;/p&gt;
 Remote for Google Slides works on everywhere 






&lt;p&gt;Try out Remote for Slides today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://chrome.google.com/webstore/detail/remote-for-slides/pojijacppbhikhkmegdoechbfiiibppi?hl=en" rel="noopener noreferrer"&gt;Remote for Slides Chrome Extension&lt;/a&gt; (Chrome Web Store)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://slides.limhenry.xyz/" rel="noopener noreferrer"&gt;Remote for Slides Progressive Web App&lt;/a&gt; (s.limhenry.xyz)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://play.google.com/store/apps/details?id=xyz.limhenry.slides.lite" rel="noopener noreferrer"&gt;Remote for Slides Lite Android App&lt;/a&gt; (Google Play Store)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;We’d love to hear what you think. Please let us know how we can improve on &lt;a href="https://twitter.com/remoteforslides" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; (&lt;a href="https://twitter.com/remoteforslides" rel="noopener noreferrer"&gt;@remoteforslides&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Want to support this project? You can &lt;strong&gt;&lt;a href="https://www.paypal.me/henrylim96" rel="noopener noreferrer"&gt;make a donation here&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>web</category>
      <category>android</category>
      <category>googleslides</category>
    </item>
    <item>
      <title>Case Study: Preview Your Launcher Icon Design with icon.limhenry.xyz</title>
      <dc:creator>Henry Lim</dc:creator>
      <pubDate>Thu, 21 Mar 2019 06:10:37 +0000</pubDate>
      <link>https://forem.com/henrylim96/case-study-preview-your-launcher-icon-design-with-iconpreviewcom-1k53</link>
      <guid>https://forem.com/henrylim96/case-study-preview-your-launcher-icon-design-with-iconpreviewcom-1k53</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdnjsyrd0zd4nomyijisa.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdnjsyrd0zd4nomyijisa.jpg" width="750" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;"Launcher Icon Previewer allows you to preview your launcher icon design on your mobile devices."&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://icon.limhenry.xyz/" rel="noopener noreferrer"&gt;Launcher Icon Previewer&lt;/a&gt; is a project created by me and &lt;a href="https://medium.com/@taylorling" rel="noopener noreferrer"&gt;Taylor Ling&lt;/a&gt;, the co-founder of Fabulous app, and he’s also a Google Product Design Expert.&lt;/p&gt;

&lt;p&gt;You can learn more about Launcher Icon Previewer here: &lt;strong&gt;&lt;a href="https://icon.limhenry.xyz/" rel="noopener noreferrer"&gt;icon.limhenry.xyz&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post was originally published on &lt;a href="https://medium.com/@limhenry/case-study-launcher-icon-previewer-b5a1fc959db2" rel="noopener noreferrer"&gt;medium.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Technologies
&lt;/h3&gt;

&lt;p&gt;We decided to use all the greatest and latest technologies to develop Launcher Icon Previewer, which includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/database/" rel="noopener noreferrer"&gt;Firebase Realtime Database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/hosting/" rel="noopener noreferrer"&gt;Firebase Hosting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/storage/" rel="noopener noreferrer"&gt;Cloud Storage for Firebase&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://firebase.google.com/docs/functions/" rel="noopener noreferrer"&gt;Cloud Functions for Firebase&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.google.com/web/fundamentals/app-install-banners/" rel="noopener noreferrer"&gt;Add to Home Screen&lt;/a&gt;, &lt;a href="https://developers.google.com/web/fundamentals/web-app-manifest/" rel="noopener noreferrer"&gt;Web App Manifest&lt;/a&gt; and &lt;a href="https://developers.google.com/web/fundamentals/primers/service-workers/" rel="noopener noreferrer"&gt;Service Worker&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://expressjs.com/" rel="noopener noreferrer"&gt;Express&lt;/a&gt; and &lt;a href="http://ejs.co/" rel="noopener noreferrer"&gt;EJS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Home &amp;amp; Upload Page
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpgax0hcrbuhmwzd27bk1.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpgax0hcrbuhmwzd27bk1.jpeg" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;
"Home &amp;amp; Upload Page" (Select Icon page, Preview Icon page, and Uploading page)



&lt;p&gt;The "Home &amp;amp; Upload Page" consists of 3 parts: &lt;em&gt;Select Icon&lt;/em&gt;, &lt;em&gt;Preview Icon&lt;/em&gt;, and &lt;em&gt;Uploading&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;On the "Preview Icon*" page, once the user clicked on the blue "Upload Icon" button, the icon will upload to the Cloud Storage. Once the icon is uploaded, it will return a download URL for the icon.&lt;/p&gt;

&lt;p&gt;After that, we will store the icon’s URL, together with other information like the icon’s dimension, icon’s file type, app name, timestamp to Firebase Realtime Database.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Once everything is completed, the page will redirect to the "Preview &amp;amp; Share Page".&lt;/p&gt;

&lt;h4&gt;
  
  
  Preview &amp;amp; Share Page
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2rflhrypx0c1n1hnty2p.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2rflhrypx0c1n1hnty2p.jpeg" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;
"Preview &amp;amp; Share Page"



&lt;p&gt;&lt;strong&gt;Generate QR Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We added a QR code on the "Share Page", this will make it easier for the user to open the page on another device. In this case, we are actually using a &lt;a href="https://developers.google.com/chart/infographics/docs/qr_codes" rel="noopener noreferrer"&gt;deprecated Google Charts API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8idkhcyq1m674v4khwuk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8idkhcyq1m674v4khwuk.png" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;
Well, it’s still working perfectly ¯\_(ツ)_/¯



&lt;p&gt;By using the URL below, we are able to generate the QR code on the fly with a URL GET request.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://chart.googleapis.com/chart?chs=256x256&amp;amp;cht=qr&amp;amp;chld=L|1&amp;amp;choe=UTF-8&amp;amp;chl=https://icon.limhenry.xyz/123456&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service Worker&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s important to register the service worker correctly since one of the criteria for add to home screen is to have a registered service worker.&lt;/p&gt;

&lt;p&gt;In this case, we need to set the scope of the service worker to &lt;code&gt;/&amp;lt;id&amp;gt;/&lt;/code&gt; instead of the root of the page. By doing this, the service worker will only control the requests of the page from &lt;code&gt;icon.limhenry.xyz/&amp;lt;id&amp;gt;/&lt;/code&gt;, but not from &lt;code&gt;icon.limhenry.xyz/&lt;/code&gt;.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Web App Manifest &amp;amp; Add to Home Screen&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1ahzmcoj0iq7aqqp6ef.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1ahzmcoj0iq7aqqp6ef.png" width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;
Adding Your App Icon to the Home Screen (Chrome for Android)



&lt;p&gt;In order to show the "Add to Home Screen" button in the preview page &lt;em&gt;(see screenshot above)&lt;/em&gt;, we are using the &lt;code&gt;beforeinstallprompt&lt;/code&gt; event to show the button on the screen:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
The browser will fire a "beforeinstallprompt" event is the site meets the Add to Home screen criteria




&lt;p&gt;Take note this &lt;code&gt;beforeinstallprompt&lt;/code&gt; event is still an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/BeforeInstallPromptEvent" rel="noopener noreferrer"&gt;experimental technology&lt;/a&gt;, and it’s only working on Chrome 45 and above. However, for those using browsers that don’t support the &lt;code&gt;beforeinstallprompt&lt;/code&gt; event, they still can add the page to the home screen from the menu options.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
Dynamically generate "manifest.json" using Express




&lt;p&gt;To generate the web app manifest dynamically, we are using Express which is running on the Cloud Functions for Firebase. This allows us to generate a custom web app manifest with different app icon, start URL and app name on the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-Platform Support&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Initially, we built this Launcher Icon Previewer just for the Chrome browser, but we tried our best to make sure it works fine on other platforms too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3a9q93lxqdas308hmg1g.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3a9q93lxqdas308hmg1g.jpeg" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;
Launcher Icon Previewer running on Safari for iOS



&lt;p&gt;In this case, we want to make sure Launcher Icon Previewer is working on Safari for iOS. However, there’s a problem: Safari doesn’t support web app manifest. &lt;/p&gt;

&lt;p&gt;Meet &lt;a href="https://github.com/GoogleChromeLabs/pwacompat" rel="noopener noreferrer"&gt;PWACompat&lt;/a&gt;, PWACompat is a library that brings the Web App Manifest to non-compliant browsers for better Progressive Web Apps. By adding PWACompat to the site, it will dynamically generate the appropriate meta icon tags needed to meet to criteria for add to home screen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc3lmqtl5yhrbfct9di1b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc3lmqtl5yhrbfct9di1b.png" width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;
Launcher Icon Previewer running on the desktop (Chrome for MacOS)



&lt;p&gt;Furthermore, with the support of &lt;a href="https://developers.google.com/web/progressive-web-apps/desktop" rel="noopener noreferrer"&gt;Desktop Progressive Web App&lt;/a&gt; on the latest version of Google Chrome for Chrome OS, MacOS, Windows and Linux, Launcher Icon Previewer will also work on the desktop platform.&lt;/p&gt;

&lt;p&gt;In conclusion, the user can now preview their app icon on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App Drawer, Home Screen (Android &amp;amp; iOS)&lt;/li&gt;
&lt;li&gt;Start menu, Taskbar (Windows)&lt;/li&gt;
&lt;li&gt;Launchpad, Dock (MacOS)&lt;/li&gt;
&lt;li&gt;App Launcher, Shelf (ChromeOS)&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  &lt;strong&gt;Optimization: Auto delete icons after 24 hours&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;This project is currently using Firebase Spark (free) plan, hence we only can store total 5GB of data per month on Cloud Storage for Firebase, with 1GB of download traffic per day.&lt;/p&gt;

&lt;p&gt;To prevent us from exceeding the limit, we need to delete the old, unused icon files from Cloud Storage periodically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq65d2ap4al4cuylv0ybq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq65d2ap4al4cuylv0ybq.png" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
Dashboard of cron-job.org



&lt;p&gt;In this case, we are using &lt;a href="https://cron-job.org/en/" rel="noopener noreferrer"&gt;cron-job.org&lt;/a&gt; to schedule the Cloud Functions to execute every 24 hours.&lt;/p&gt;

&lt;p&gt;The Cloud Functions will check the upload timestamp, and it will delete the icon from the Cloud Storage if it’s older than 24 hours. By using this method, we can &lt;em&gt;(hopefully)&lt;/em&gt; make sure this project can runs perfectly without exceeding the limit of Firebase free plan.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5trv6sjwvha489qvdqcq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5trv6sjwvha489qvdqcq.png" width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://icon.limhenry.xyz/" rel="noopener noreferrer"&gt;Launcher Icon Previewer&lt;/a&gt; is currently in beta, as we aren’t sure if there will be any more issue, but feel free to share it out! 🎉&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Psst, are you a public speaker or you are going to give a presentation soon?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Try out &lt;a href="https://chrome.google.com/webstore/detail/remote-for-slides/pojijacppbhikhkmegdoechbfiiibppi?hl=en" rel="noopener noreferrer"&gt;Remote for Slides&lt;/a&gt;. It is a Chrome Extension and a Progressive Web App that allows you to control Google Slides on any device, remotely, without the need of any extra hardware.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Want to support these projects? You can&lt;/em&gt; &lt;em&gt;&lt;a href="http://bit.ly/slides-donate" rel="noopener noreferrer"&gt;make a donation here&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>pwa</category>
      <category>uiux</category>
      <category>firebase</category>
    </item>
    <item>
      <title>30 Events &amp; 10 Websites later — My journey with Google Developer Group Kuala Lumpur</title>
      <dc:creator>Henry Lim</dc:creator>
      <pubDate>Mon, 18 Mar 2019 14:37:21 +0000</pubDate>
      <link>https://forem.com/henrylim96/30-events--10-websites-later--my-journey-with-gdg-kuala-lumpur-470i</link>
      <guid>https://forem.com/henrylim96/30-events--10-websites-later--my-journey-with-gdg-kuala-lumpur-470i</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp28obd9838wfjfpnl86r.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp28obd9838wfjfpnl86r.jpeg" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;
I/O Attendees from Asia Pacific @ Google I/O 2017






&lt;p&gt;&lt;em&gt;This post was originally published on &lt;a href="https://medium.com/@limhenry/my-journey-with-gdg-kuala-lumpur-81e5b82ed7ed" rel="noopener noreferrer"&gt;medium.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; On October 2015, I joined GDG Kuala Lumpur as a committee member. In the last 3 years, I have learned and grown a lot with this amazing community. Without all of this, I would not be who I am today.&lt;/p&gt;

&lt;p&gt;3 years later, after I volunteered and organized more than 30 events, I stepped down the role of the main organizer at GDG Kuala Lumpur, and say goodbye to this big family.&lt;/p&gt;

&lt;p&gt;Here’s my journey from the past few years:&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What is Google Developer Groups?&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Google Developer Groups&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;(GDGs) are local groups of developers who are specifically interested in Google products and APIs. Each local group is called a GDG chapter and can host a variety of technical activities for developers - from just a few people getting together to watch our latest videos, to large gatherings with demos and tech talks, to hackathons.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  2014: The beginning of this epic journey
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event Volunteered: 2&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;June 2014&lt;/strong&gt;: It was the beginning of the journey: My first year as a computer science student at Multimedia University, Cyberjaya.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq2vrpi8t3heaxagkgl9e.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq2vrpi8t3heaxagkgl9e.jpeg" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;
The campus of my university: Multimedia University, Cyberjaya



&lt;p&gt;Few months later, I found out there was a Google program happening on my campus: the Google Student Ambassador Program.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The&lt;/em&gt; &lt;strong&gt;&lt;em&gt;Google Student Ambassador (GSA) Program&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;was a global program that was aimed at active tertiary students from all academic backgrounds. It was an opportunity for students to act as liaisons between Google and their universities and also an opportunity for Google to contribute to the education of future leaders.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was a big fan of Google technologies during that time. Unfortunately, I was too late to apply to become one of the GSA. Opportunely, I was lucky enough to have the chance to become a volunteer at some of the events organized by the GSA at that time: &lt;a href="https://medium.com/@anonoz" rel="noopener noreferrer"&gt;Anonoz Chong&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyzpa8ww4yhso00au9aqe.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyzpa8ww4yhso00au9aqe.jpeg" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
Android Meetup, Multimedia University, Cyberjaya (2014)



&lt;h3&gt;
  
  
  2015: Joining GDG Kuala Lumpur + My Very First Tech Talk
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event Organized: 2&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event Website: 2&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Public Speaking: 1&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;July 2015:&lt;/strong&gt; I attended my first GDG Kuala Lumpur event — I/O Extended Kuala Lumpur 2015. This was where I met &lt;a href="https://medium.com/@tyohan" rel="noopener noreferrer"&gt;Yohan Totting&lt;/a&gt; for the first time and learned about the Google Developer Experts program.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Google Developers Experts&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;(GDEs) are a global network of experienced product strategists, designers, developers, and marketing professionals actively&lt;br&gt;
supporting developers, startups, and companies changing the world through web and mobile applications.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuh03ucc5pq6h85gbf4fw.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuh03ucc5pq6h85gbf4fw.jpeg" width="800" height="726"&gt;&lt;/a&gt;&lt;/p&gt;
Yohan Totting was the first Google Developer Expert I ever met — I/O Extended Kuala Lumpur 2015



&lt;p&gt;&lt;strong&gt;October 2015:&lt;/strong&gt; All thanks to &lt;a href="https://medium.com/@liewjuntung" rel="noopener noreferrer"&gt;JT Liew&lt;/a&gt;, I got the chance to use the knowledge I learned from the past few months: Polymer and Firebase, to build my first website for one of the GDG Kuala Lumpur’s event: GDG DevFest Kuala Lumpur 2015. This was my first time involved in a GDG Kuala Lumpur event.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9x2amhq9v9veb9ckvnu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9x2amhq9v9veb9ckvnu7.png" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
My very first website for GDG Kuala Lumpur event: GDG DevFest Kuala Lumpur 2015



&lt;p&gt;&lt;strong&gt;December 2015:&lt;/strong&gt; Also at the same year, I started to fall in love with public speaking. I always love to share my knowledge with everyone around me. Hence, I gave my very first talk about Polymer and Firebase at BarCamp Cyberjaya 2015.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft1x4ui0yysvb0taolyez.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft1x4ui0yysvb0taolyez.jpeg" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;
Me talking about Polymer &amp;amp; Firebase — BarCamp Cyberjaya 2015



&lt;h3&gt;
  
  
  2016: More Events, More Talks, Lucky Year
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event Organized/Volunteered: 10&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event Website:&lt;/strong&gt; &lt;strong&gt;4&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Public Speaking: 7&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;February/March 2016:&lt;/strong&gt; I found out there was a worldwide virtual competition organized by Firebase called &lt;a href="https://firebase.googleblog.com/2016/02/test-your-skills-at-static-showdown_20.html" rel="noopener noreferrer"&gt;Static Showdown&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Without the slightest hesitation, I decided to join the hackathon, and I built a Progressive Web App using Polymer and Firebase. One week later, out of my surprise, I won the Best Progressive Web App category! Thanks, &lt;a href="https://medium.com/@mbleigh" rel="noopener noreferrer"&gt;Michael Bleigh&lt;/a&gt; and Firebase for this amazing competition!&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-705204198229422080-393" src="https://platform.twitter.com/embed/Tweet.html?id=705204198229422080"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-705204198229422080-393');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=705204198229422080&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;May 2016:&lt;/strong&gt; Few months after the Static Showdown hackathon, I found out there was &lt;em&gt;another&lt;/em&gt; competition organized by Firebase: &lt;a href="https://firebase.googleblog.com/2016/06/behind-scenes-firebass-arg-challenge.html" rel="noopener noreferrer"&gt;Firebass ARG Challenge&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-736301472770494464-329" src="https://platform.twitter.com/embed/Tweet.html?id=736301472770494464"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-736301472770494464-329');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=736301472770494464&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;** &lt;em&gt;Drum Roll&lt;/em&gt; ** and … I completed the challenge and I became the 51st person (out of 100 winners) to complete the challenge and won a free ticket to Google I/O 2017!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;July 2016:&lt;/strong&gt; I gave a talk about Progressive Web App and Polymer together with 2 amazing Googlers: &lt;a href="https://medium.com/@bokty" rel="noopener noreferrer"&gt;Bok Thye Yeow&lt;/a&gt; and &lt;a href="https://medium.com/@mfoon" rel="noopener noreferrer"&gt;Marcus Foon&lt;/a&gt; at my university. Over 200 people attended the event!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvjace0i8vqktgr8iqdc0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvjace0i8vqktgr8iqdc0.jpeg" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;
Me showing off my T-Rex pillow — Tech Talk Thursday with Google &amp;amp; GDGKL, Multimedia University, Cyberjaya



&lt;p&gt;&lt;strong&gt;July 2016:&lt;/strong&gt; I shared my story of winning the Firebase Challenge at I/O Extended Kuala Lumpur 2016. I was also part of the organizing committees for that conference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbsx9ybbe6q8lvkwzwqte.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbsx9ybbe6q8lvkwzwqte.jpeg" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;
Me sharing my story of winning the Firebass Challenge — I/O Extended Kuala Lumpur 2016



&lt;p&gt;&lt;strong&gt;Nov 2016:&lt;/strong&gt; I was fortunate enough to get the chance to receive the invite from Google DevRel team to attend Progressive Web App Roadshow in Bangkok. &lt;em&gt;(Thanks &lt;a href="https://medium.com/@chelleobligaciongray" rel="noopener noreferrer"&gt;Chelle&lt;/a&gt; and &lt;a href="https://medium.com/@bokty" rel="noopener noreferrer"&gt;Bok&lt;/a&gt;!)&lt;/em&gt;. It was my first time to attend a tech event outside Malaysia.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw6azfz7jgbsephmowamx.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw6azfz7jgbsephmowamx.jpeg" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;
Chrome Team x GDG Thailand x GDG Kuala Lumpur — PWA Roadshow Bangkok 2016, Thailand



&lt;h3&gt;
  
  
  2017: More Events, Websites &amp;amp; Talks + GDG Summit &amp;amp; Google I/O
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event Organized/Volunteered: 11&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event Website: 5&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Public Speaking: 6&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jih9mym0w7v2hh11lzc.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jih9mym0w7v2hh11lzc.jpeg" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;
¯\_(ツ)_/¯ — JomCode Kuala Lumpur 2017, Google Malaysia Office



&lt;p&gt;&lt;strong&gt;April 2017:&lt;/strong&gt; With the help from &lt;a href="https://medium.com/@naz.quadir" rel="noopener noreferrer"&gt;Nazlina Quadir&lt;/a&gt; and Google Malaysia, I was lucky enough to have the chance to share my story of winning the Firebass Challenge with two local media outlets: &lt;a href="https://www.digitalnewsasia.com/mobility/student-nets-elusive-firebass-ticket-google%C3%A2%C2%80%C2%99s-io-2017" rel="noopener noreferrer"&gt;Digital News Asia&lt;/a&gt; and &lt;a href="http://www.orientaldaily.com.my/s/191483" rel="noopener noreferrer"&gt;Oriental Daily (東方日報)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fajuzpp30kcx4bikp6drs.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fajuzpp30kcx4bikp6drs.jpeg" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;
Hey mom, I got featured in the newspaper!



&lt;p&gt;&lt;strong&gt;May 2017:&lt;/strong&gt; I attended my first Global GDG Leaders Summit and Google I/O 2017 in Bay Area, California. This was also my first time traveled to a different continent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv00qvhkrvzs5zxltqwod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv00qvhkrvzs5zxltqwod.png" width="640" height="471"&gt;&lt;/a&gt;&lt;/p&gt;
🇲🇾 GDE x GDG x Googler — I/O 2017, Mountain View, United States



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjr3gwz7oz98uq721t7d.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyjr3gwz7oz98uq721t7d.jpeg" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;
Can you find where I am? — GDG Global Summit 2017, San Jose, United States



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh524fyg5i60ztw7l3bc0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh524fyg5i60ztw7l3bc0.png" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;
Firebass Challenge Winner @ Google I/O 2017



&lt;p&gt;&lt;strong&gt;July 2018:&lt;/strong&gt; I celebrated my 21st birthday with the GDG Kuala Lumpur family at I/O Extended Kuala Lumpur 2017. Thanks &lt;a href="https://medium.com/@chelleogray" rel="noopener noreferrer"&gt;Chelle Gray&lt;/a&gt; for the super cute cake! &lt;em&gt;(Finally I can drink alcohol legally)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnt645ue71wyhmdmzujke.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnt645ue71wyhmdmzujke.jpeg" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;September 2017:&lt;/strong&gt; We had a community booth at Google Cloud Summit Kuala Lumpur together with the TensorFlow User Group Malaysia. What a fun day!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F925sq130z9svefyrgjf4.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F925sq130z9svefyrgjf4.jpeg" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;
GDG Kuala Lumpur Community Booth — Google Cloud Summit Kuala Lumpur 2017



&lt;p&gt;&lt;strong&gt;October 2017:&lt;/strong&gt; The GDG Kuala Lumpur committees joined the Firebase AppFest Hackathon Malaysia and … we won the second prize! Hooray! &lt;em&gt;&lt;a href="https://medium.com/@limhenry/how-we-crashed-the-firebase-console-in-front-of-firebase-engineers-a04afac72095" rel="noopener noreferrer"&gt;(Read the full story here.)&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Fun fact: This was my third time winning the competition held by the Firebase team! Thank you Firebase!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbykq908n415b2i2cyuvy.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbykq908n415b2i2cyuvy.jpeg" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;
GDE x GDG x Googler — Firebase AppFest Hackathon Malaysia 2017



&lt;h3&gt;
  
  
  2018: Became a GDE + Time to Say Goodbye to GDGKL
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event Organized: 5&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event Website: 3&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Public Speaking: 11&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Feb 2018:&lt;/strong&gt; I was referred by &lt;a href="https://medium.com/@robertnyman" rel="noopener noreferrer"&gt;Robert Nyman&lt;/a&gt; to become a Google Developer Expert in Web Technologies. OMG!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu80bqwlz0fckqrwvqnxe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu80bqwlz0fckqrwvqnxe.png" width="800" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;April 2018:&lt;/strong&gt; It marked another major milestone in my life. I passed the Google Developer Expert interview and became the youngest Google Developer Expert in South East Asia! &lt;em&gt;&lt;a href="https://medium.com/@limhenry/i-am-now-a-google-developer-expert-in-web-technologies-64be25c29812" rel="noopener noreferrer"&gt;(Read the full story here.)&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft4ufhxxmb8fpw5kn5spr.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft4ufhxxmb8fpw5kn5spr.jpeg" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;
South East Asia Google Developer Experts — GDE Summit 2018, Sunnyvale, United States



&lt;p&gt;&lt;strong&gt;July 2018:&lt;/strong&gt; I organized my last conference for GDG Kuala Lumpur — I/O Extended Kuala Lumpur 2018. The conference was a huge success: over 400 people attended the event.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft5ovq6nc5l54irwiy457.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft5ovq6nc5l54irwiy457.jpeg" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;
Speaker dinner for I/O Extended Kuala Lumpur 2018



&lt;p&gt;At the same time, we also broke the record of the turn-up rate for GDG Kuala Lumpur events: 96.25%. Kudo to the teams: &lt;a href="https://medium.com/@jecelynyeen" rel="noopener noreferrer"&gt;Jecelyn Yeen&lt;/a&gt;, &lt;a href="https://medium.com/@shangyilim" rel="noopener noreferrer"&gt;Lim Shang Yi&lt;/a&gt; and Eric Ngu!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy7m2nawxntduupx477pk.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy7m2nawxntduupx477pk.jpeg" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;
I/O Extended Kuala Lumpur 2018 Core Organizing Team



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwm8qhihont33vcl8nl58.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwm8qhihont33vcl8nl58.jpeg" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;
Group Photo! — I/O Extended Kuala Lumpur 2018



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8784x3jfrgnuztojuc05.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8784x3jfrgnuztojuc05.jpeg" width="800" height="528"&gt;&lt;/a&gt;&lt;/p&gt;
I/O Extended Kuala Lumpur 2018 Progressive Web App



&lt;p&gt;&lt;strong&gt;October 2018:&lt;/strong&gt; All good things must come to an end. After organized more than 30 events and developed more than 10 websites for GDG Kuala Lumpur, it was time to say goodbye to GDG Kuala Lumpur. I stepped down as GDG Kuala Lumpur organizer, and moved to Bangkok to begin my next step in my career.&lt;/p&gt;

&lt;p&gt;Goodbye GDG Kuala Lumpur! Goodbye Malaysia! You will be missed. 👋🏻&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffoykbquor3v7z55dd6kn.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffoykbquor3v7z55dd6kn.jpeg" width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;
All the websites I designed for GDG Kuala Lumpur (2015–2019)






&lt;h3&gt;
  
  
  &lt;em&gt;"GDGKL is not only an ordinary tech community, but it’s a community with lots of love. This is a big family." — Me&lt;/em&gt;
&lt;/h3&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F09dxlilvojcz51uy41pk.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F09dxlilvojcz51uy41pk.jpeg" width="800" height="613"&gt;&lt;/a&gt;&lt;/p&gt;
Photos with the GDGKL Family



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqf3nz40fku8q0yh1pd3.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqf3nz40fku8q0yh1pd3.jpeg" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;
Selfieeeeee with Googlers (DevRel, Web/Chrome, Firebase, Flutter, and Material Design)



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjluxqym1c2l13qjlqt1u.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjluxqym1c2l13qjlqt1u.jpeg" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmqok2t71idrrqvwivg5.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmqok2t71idrrqvwivg5.jpeg" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;
One of my favorite Googler: Chelle Obligacion-Gray (Now a Xoogler)



&lt;p&gt;If you ask me, what’s the best part of being a committee of a GDG chapter? I would say it feels like a big family.&lt;/p&gt;

&lt;p&gt;It has been an honor and pleasure to have the chance to meet with all of these awesome local developers, GDG volunteers and committees, Google Developer Experts, and Googlers from all over the world.&lt;/p&gt;

&lt;h3&gt;
  
  
  What’s Next?
&lt;/h3&gt;

&lt;p&gt;I still have a long journey to go. All of these are just the tip of the iceberg. Although I am not part of the GDG Kuala Lumpur committee anymore, my heart is still with them.&lt;/p&gt;

&lt;p&gt;In the end, I wanted to say thank you to everyone that guided me, helped me, supported me for the past 4 years. Without your help and support, all of these would not have been possible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fegiy3e4kae5u49mmtrma.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fegiy3e4kae5u49mmtrma.gif" width="400" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;THANK YOU&lt;/strong&gt;, &lt;a href="https://medium.com/@anonoz" rel="noopener noreferrer"&gt;Anonoz Chong&lt;/a&gt;, &lt;a href="https://medium.com/@liewjuntung" rel="noopener noreferrer"&gt;JT Liew&lt;/a&gt;, &lt;a href="https://medium.com/@chelleogray" rel="noopener noreferrer"&gt;Chelle Gray&lt;/a&gt;, &lt;a href="https://medium.com/@bokty" rel="noopener noreferrer"&gt;Bok Thye Yeow&lt;/a&gt;, &lt;a href="https://medium.com/@jecelynyeen" rel="noopener noreferrer"&gt;Jecelyn Yeen&lt;/a&gt; and everyone else for everything in the past few years.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp08kb67lh7l3p8ehunfx.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp08kb67lh7l3p8ehunfx.jpeg" width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Note:&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;I am currently still active in the Google Developers community as a Google Developer Expert. I stepped down the role of the main organizer at Google Developer Group Kuala Lumpur as I don’t live in Malaysia anymore. Anyway, if you are going to Google I/O 2019, see you there!&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Psst, are you a public speaker or you are going to give a presentation soon?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Try out&lt;/em&gt; &lt;strong&gt;&lt;em&gt;&lt;a href="https://chrome.google.com/webstore/detail/remote-for-slides/pojijacppbhikhkmegdoechbfiiibppi?hl=en" rel="noopener noreferrer"&gt;Remote for Slides&lt;/a&gt;&lt;/em&gt;&lt;/strong&gt;. &lt;em&gt;It is a Chrome Extension and a Progressive Web App that allows you to control Google Slides on any device, remotely, without the need of any extra hardware.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gdg</category>
      <category>gde</category>
      <category>google</category>
      <category>community</category>
    </item>
  </channel>
</rss>
