<?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>DEV Community: Sophie F A</title>
    <description>The latest articles on DEV Community by Sophie F A (@sophie_fa_6ed935b0601d76).</description>
    <link>https://dev.to/sophie_fa_6ed935b0601d76</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4029791%2Fa9280f9a-134e-4ad5-87cb-1d0f1d6c6cc2.png</url>
      <title>DEV Community: Sophie F A</title>
      <link>https://dev.to/sophie_fa_6ed935b0601d76</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sophie_fa_6ed935b0601d76"/>
    <language>en</language>
    <item>
      <title>Cross-Platform Design Tokens — Getting Web + Mobile to Feel Like One Product</title>
      <dc:creator>Sophie F A</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:13:40 +0000</pubDate>
      <link>https://dev.to/sophie_fa_6ed935b0601d76/cross-platform-design-tokens-getting-web-mobile-to-feel-like-one-product-2ibh</link>
      <guid>https://dev.to/sophie_fa_6ed935b0601d76/cross-platform-design-tokens-getting-web-mobile-to-feel-like-one-product-2ibh</guid>
      <description>&lt;p&gt;Most full-stack SaaS templates ship a Next.js web app and a React Native mobile app in the same repo. Fewer share design tokens between them. And that's the difference between an app that feels like one product on two surfaces and an app that feels like two companies wearing the same t-shirt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The visual drift problem
&lt;/h2&gt;

&lt;p&gt;Open a random full-stack SaaS template. Compare the primary button color on the web dashboard to the primary button color on the mobile app. Are they exactly the same? Same shade, same weight, same radius, same padding? In most templates, no — they're close but not identical, because the web team picked one hex and the mobile team picked another, and there's no shared source of truth.&lt;/p&gt;

&lt;p&gt;That drift compounds. Six months later, the web app has ten shades of blue, the mobile app has eight, and neither team can tell you which is the "brand" blue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shared tokens actually mean
&lt;/h2&gt;

&lt;p&gt;Shared design tokens for a full-stack SaaS means one JSON (or TypeScript) file that defines every visual value the app uses, and both the web app and the mobile app import from it.&lt;/p&gt;

&lt;p&gt;tokens/&lt;br&gt;
  colors.ts      // brand, surface, text, border, semantic (success/warning/error)&lt;br&gt;
  typography.ts  // heading scale, body scale, mono&lt;br&gt;
  spacing.ts     // 4px base unit, exposed as space.1, space.2, space.4, space.8&lt;br&gt;
  radius.ts      // sm, md, lg, full&lt;br&gt;
  shadow.ts      // sm, md, lg&lt;/p&gt;

&lt;p&gt;The web app consumes them via Tailwind config or a CSS-in-JS theme. The mobile app consumes them via a theme provider that resolves platform-appropriate values. Same source, different runtimes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where web and mobile should diverge
&lt;/h2&gt;

&lt;p&gt;Sharing tokens does not mean web and mobile should look identical. There are good reasons to diverge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Typography:&lt;/strong&gt; iOS has SF Pro, Android has Roboto (or Noto), the web can have anything. Shared tokens define the scale (heading.xl = 32px); platform code picks the family.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Touch targets:&lt;/strong&gt; mobile needs a 44pt minimum tap target, web can be smaller. Shared spacing tokens, different application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elevation:&lt;/strong&gt; mobile uses subtle shadows or none (Material) plus platform blur effects; web can use heavier shadows because there's more visual noise to compete with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corner radius:&lt;/strong&gt; iOS trends smaller (8-12px), Android trends larger (16-24px), web is a taste call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A well-designed shared tokens system exposes the primitives (color, spacing, scale) and lets the platform layer pick the composition. Templates that force pixel-identical layouts across web and mobile end up making both surfaces feel wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auditing a template's design layer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search the codebase for hex codes.&lt;/strong&gt; If there are more than ~20 outside the tokens file, the template has drifted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at the tokens file.&lt;/strong&gt; Is it semantic (text.subtle) or raw (gray800)? Semantic wins for theming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check dark mode.&lt;/strong&gt; Is it token-based (each token has a light + dark value, resolved at the provider) or if (isDark) scattered through screens? The former scales, the latter doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for a component gallery&lt;/strong&gt; (Storybook, or a /design route). Templates that shipped one thought about design as a system, not screens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for component variant definitions in the theme,&lt;/strong&gt; not the component. Button.variants.primary in theme &amp;gt;  with hardcoded styles.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The handoff pattern that scales
&lt;/h2&gt;

&lt;p&gt;[7:02 AM]The design handoff pattern that works long-term for full-stack SaaS templates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Designer works in Figma with a token library that mirrors the code tokens.&lt;/li&gt;
&lt;li&gt;When tokens change (a new brand color, an updated radius scale), the designer edits Figma and files a PR against the tokens file with the same names.&lt;/li&gt;
&lt;li&gt;Engineering merges the PR, tokens flow to both web and mobile instantly, no screen touched.&lt;/li&gt;
&lt;li&gt;Component names in Figma match component names in code — PrimaryButton in both places.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Templates that ship this pattern from day one save the design team weeks of translation work per project. Templates that don't force every design change to become an engineering task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applighter's approach
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://applighter.com" rel="noopener noreferrer"&gt;Applighter&lt;/a&gt;, the cross-platform tokens layer is treated as first-class — one tokens file consumed by both the Next.js web app and the Expo mobile app, semantic naming throughout, platform-appropriate composition at the render layer, Figma library that mirrors code component names. Not because we're perfect on this — because we watched too many full-stack templates ship with a drift problem baked in.&lt;/p&gt;

&lt;p&gt;Whatever template you evaluate, ask this one question: is there a single tokens file that both surfaces import? If yes, the design work will scale. If no, budget the retrofit.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>designsystems</category>
      <category>react</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>React Native i18n in 2026: A Practical Expo Router Playbook</title>
      <dc:creator>Sophie F A</dc:creator>
      <pubDate>Mon, 03 Aug 2026 07:35:54 +0000</pubDate>
      <link>https://dev.to/sophie_fa_6ed935b0601d76/react-native-i18n-in-2026-a-practical-expo-router-playbook-2k8f</link>
      <guid>https://dev.to/sophie_fa_6ed935b0601d76/react-native-i18n-in-2026-a-practical-expo-router-playbook-2k8f</guid>
      <description>&lt;p&gt;If your React Native app only speaks English, you're leaving ~75% of smartphone users on the table. Here's the modern stack for shipping a multilingual Expo app without three days of glue code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;react-i18next&lt;/strong&gt;: translation management (6M+ weekly downloads, TS-first)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;expo-localization&lt;/strong&gt;: device locale detection (or &lt;code&gt;react-native-localize&lt;/code&gt; for bare RN)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intl&lt;/strong&gt;: dates, numbers, currencies (built into Hermes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I18nManager&lt;/strong&gt;: RTL layout flipping&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup in 4 Steps
&lt;/h2&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx expo &lt;span class="nb"&gt;install &lt;/span&gt;expo-localization
npm i i18next react-i18next
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Translation files
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;locales/
  en.json
  es.json
  ar.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"welcome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cart"&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;"itemCount_one"&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}} item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"itemCount_other"&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}} items"&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;Use the ICU plural suffixes (&lt;code&gt;_one&lt;/code&gt;, &lt;code&gt;_other&lt;/code&gt;, &lt;code&gt;_few&lt;/code&gt;, &lt;code&gt;_many&lt;/code&gt;, &lt;code&gt;_zero&lt;/code&gt;). Arabic has 6 plural forms, so get this right on day one.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Bootstrap
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// i18n.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;i18n&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;i18next&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;initReactI18next&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;react-i18next&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Localization&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;expo-localization&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;en&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;./locales/en.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;import&lt;/span&gt; &lt;span class="nx"&gt;es&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;./locales/es.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;import&lt;/span&gt; &lt;span class="nx"&gt;ar&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;./locales/ar.json&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;deviceLocale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Localization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLocales&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;languageCode&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;i18n&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;initReactI18next&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;compatibilityJSON&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;translation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;es&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;translation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;es&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;ar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;translation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ar&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;deviceLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fallbackLng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;interpolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;escapeValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&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;i18n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two production-important bits: &lt;code&gt;compatibilityJSON: 'v4'&lt;/code&gt; for modern plural rules, and &lt;code&gt;escapeValue: false&lt;/code&gt; because RN already escapes JSX.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Wire into Expo Router
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/_layout.tsx&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;Stack&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;expo-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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Stack&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;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in any component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;useTranslation&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;react-i18next&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTranslation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart.itemCount&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="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Text&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="c1"&gt;// "3 items"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Gotchas Nobody Warns You About
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Never concatenate around &lt;code&gt;t()&lt;/code&gt;&lt;/strong&gt;. Word order changes across languages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Breaks in Japanese, German, Arabic&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&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="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Text&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// ✅ Whole sentence is one key&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;greeting&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Text&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;RTL doesn't flip everything&lt;/strong&gt;. Flexbox auto-flips, but you'll need to fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;left&lt;/code&gt;/&lt;code&gt;right&lt;/code&gt; → use &lt;code&gt;start&lt;/code&gt;/&lt;code&gt;end&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Directional icons → wrap in &lt;code&gt;transform: [{ scaleX: -1 }]&lt;/code&gt; when &lt;code&gt;I18nManager.isRTL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Hardcoded &lt;code&gt;textAlign&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hardcoded strings in third-party UI kits&lt;/strong&gt;. Buttons in libraries like RN Elements ship English defaults. Override or pick i18n-aware libs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Push notifications aren't localized by the phone&lt;/strong&gt;: your backend has to send the localized copy or use OS-level notification categories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Formatting
&lt;/h2&gt;

&lt;p&gt;Never string-concat dates or currencies. Use &lt;code&gt;Intl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dateStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NumberFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;29.99&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;@formatjs/intl-datetimeformat&lt;/code&gt; polyfills if you need older Android or less common locales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Discipline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Turn on &lt;code&gt;saveMissing&lt;/code&gt; in dev. react-i18next logs every missing key. Fail CI on any.&lt;/li&gt;
&lt;li&gt;Pseudo-localize with &lt;code&gt;en-XA&lt;/code&gt;. This surfaces hardcoded strings and 30–40% longer text for layout stress-tests.&lt;/li&gt;
&lt;li&gt;Snapshot per locale. French/German/Arabic snapshots catch layout regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Shortcut
&lt;/h2&gt;

&lt;p&gt;If you're starting fresh, &lt;a href="https://www.rapidnative.com/?utm_source=devto&amp;amp;utm_medium=blog&amp;amp;utm_campaign=mobile-app-localization-react-native" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; generates the whole i18n scaffold (provider, layout wiring, translation files, RTL flags) from a natural-language prompt. You get the same setup you'd write by hand, in about 30 seconds.&lt;/p&gt;

&lt;p&gt;Otherwise: ship it. It's 2–3 days of setup and it opens up 3 billion more users.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>expo</category>
      <category>i18n</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to Build a Video Calling App with React Native and WebRTC</title>
      <dc:creator>Sophie F A</dc:creator>
      <pubDate>Wed, 15 Jul 2026 07:22:22 +0000</pubDate>
      <link>https://dev.to/sophie_fa_6ed935b0601d76/how-to-build-a-video-calling-app-with-react-native-and-webrtc-2e3o</link>
      <guid>https://dev.to/sophie_fa_6ed935b0601d76/how-to-build-a-video-calling-app-with-react-native-and-webrtc-2e3o</guid>
      <description>&lt;p&gt;The WebRTC pipeline is where the interesting engineering lives. The UI around it — call setup screen, contact list, in-call controls, history log — is boilerplate you've built a hundred times. That's exactly the layer that &lt;a href="https://www.rapidnative.com/?utm_source=blog&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-video-calling-app-with-react-native-and-webrtc" rel="noopener noreferrer"&gt;RapidNative&lt;/a&gt; handles well: describe the screens you need, get production-ready React Native + Expo code you can drop straight into your Metro bundle.&lt;/p&gt;

&lt;p&gt;For a video calling app, that's the pre-call lobby ("Start Call" / "Join Call" / recent contacts), the in-call overlay (avatar, timer, mute/video/end buttons, participant list for group calls), and the post-call summary. You can &lt;a href="https://www.rapidnative.com/prd-to-app?utm_source=blog&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-video-calling-app-with-react-native-and-webrtc" rel="noopener noreferrer"&gt;start from a PRD&lt;/a&gt; or &lt;a href="https://www.rapidnative.com/whiteboard?utm_source=blog&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-video-calling-app-with-react-native-and-webrtc" rel="noopener noreferrer"&gt;sketch the flow&lt;/a&gt; and get a working Expo project in minutes, then wire in the &lt;code&gt;react-native-webrtc&lt;/code&gt; layer described above.&lt;/p&gt;

&lt;p&gt;The pattern generalizes: use AI to move fast on UI-heavy layers where you're implementing patterns everyone recognizes, and spend your engineering time on the parts that are genuinely hard — like the ICE candidate gathering that just failed on your test call. If you're building anything real-time, our writeups on &lt;a href="https://www.rapidnative.com/blogs/how-rapidnative-delivers-real-time-react-native-live-preview-across-every-device?utm_source=blog&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-video-calling-app-with-react-native-and-webrtc" rel="noopener noreferrer"&gt;how RapidNative delivers real-time React Native previews&lt;/a&gt; and &lt;a href="https://www.rapidnative.com/blogs/how-we-built-real-time-collaboration-for-team-app-building?utm_source=blog&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-video-calling-app-with-react-native-and-webrtc" rel="noopener noreferrer"&gt;how we built real-time collaboration for team app building&lt;/a&gt; cover the WebSocket patterns that pair nicely with WebRTC signaling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does react-native-webrtc work with Expo Go?
&lt;/h3&gt;

&lt;p&gt;No. &lt;code&gt;react-native-webrtc&lt;/code&gt; ships native iOS and Android modules that Expo Go cannot load. You need to move to Expo Dev Client or the bare workflow and use EAS Build (or a local &lt;code&gt;expo run:ios&lt;/code&gt;/&lt;code&gt;expo run:android&lt;/code&gt;) to include the native code. The &lt;a href="https://github.com/expo/config-plugins" rel="noopener noreferrer"&gt;&lt;code&gt;@config-plugins/react-native-webrtc&lt;/code&gt;&lt;/a&gt; plugin handles the required permissions automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does a WebRTC video calling app cost to run?
&lt;/h3&gt;

&lt;p&gt;For 1-to-1 calls, near zero — media flows peer-to-peer. Your only costs are the signaling server (a $5/month VPS handles thousands of concurrent calls) and TURN relay for the roughly 15–20% of calls that fall back to relayed mode. Budget $0.40 per GB of TURN traffic. Group calls with an SFU move more media through your servers and cost proportionally more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use WebRTC for one-to-many streaming?
&lt;/h3&gt;

&lt;p&gt;WebRTC is optimized for two-way conversation. For one-to-many broadcast (like a webinar or Twitch-style stream), you can use an SFU, but at scale you'll pay more than a protocol like HLS or LL-HLS designed for that pattern. WebRTC shines when latency matters more than viewer count.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between STUN and TURN?
&lt;/h3&gt;

&lt;p&gt;STUN helps a peer discover its own public IP address so two peers behind NAT can attempt a direct connection. TURN is a fallback: when direct P2P fails (usually because both peers are behind symmetric NAT), TURN acts as a media relay. STUN is cheap and free servers exist; TURN uses your bandwidth and typically costs money.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is WebRTC secure?
&lt;/h3&gt;

&lt;p&gt;Yes. All WebRTC media is encrypted with DTLS-SRTP by default — it's not optional in the spec. You cannot send unencrypted media. The signaling channel is your responsibility; use WSS (secure WebSockets) or HTTPS for whatever you build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;You now have every piece needed for a production-quality &lt;strong&gt;react native video calling app&lt;/strong&gt;: a signaling server, a peer connection lifecycle, media capture, offer/answer exchange, ICE handling, and the standard call controls. Add a TURN server, wire the UI to your app's state management, and you're 80% of the way to shipping.&lt;/p&gt;

&lt;p&gt;The hardest part of WebRTC isn't the API — it's the network. Test on real cellular connections, log ICE state transitions, and instrument connection failures early. Everything else is UI polish, and that's the part AI is happiest to write for you.&lt;/p&gt;

&lt;p&gt;Ready to build? &lt;a href="https://www.rapidnative.com/?utm_source=blog&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-video-calling-app-with-react-native-and-webrtc" rel="noopener noreferrer"&gt;Start with RapidNative&lt;/a&gt; to spin up the app shell around your WebRTC layer in minutes, or &lt;a href="https://www.rapidnative.com/blogs?utm_source=blog&amp;amp;utm_medium=content&amp;amp;utm_campaign=how-to-build-a-video-calling-app-with-react-native-and-webrtc" rel="noopener noreferrer"&gt;browse more React Native tutorials&lt;/a&gt; on the blog.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>webrtc</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
