{"id":452698,"date":"2025-03-22T03:00:12","date_gmt":"2025-03-22T03:00:12","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=452698"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=452698","title":{"rendered":"<span>A React Native &amp; Lynx i18n solution that keeps your translations organized<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>If you\u2019re building a multilingual React Native (or web) app, you\u2019ve probably tried react-i18next, i18n-js, LinguiJS, or similar libraries.<\/p>\n<p>But in every project, the same issues come up:<\/p>\n<p> \u274c Unused key-value pairs are never removed<br \/> \u274c Content gets duplicated<br \/> \u274c Ensuring format consistency across languages is painful<br \/> \u274c i18next doesn\u2019t generate TypeScript types by default \u2013 so t(&#171;my.key&#187;) won\u2019t throw even if it\u2019s been deleted<br \/> \u274c Localization platforms like Lokalise or Locize get expensive fast<\/p>\n<p>Frustrated by these challenges, I waited for a better solution&#8230; then decided to build one myself: <a href=\"https:\/\/intlayer.org\/\" rel=\"noopener noreferrer nofollow\"><strong>Intlayer<\/strong><\/a>.<\/p>\n<h3>\u2728 Key points<\/h3>\n<p> \u2705 Works with <strong>React Native<\/strong> and <strong>Lynx<\/strong><br \/> \u2705 Easy integration<br \/> \u2705 <strong>Localized content close to your components<\/strong><br \/> \u2705 <strong>Autogenerated TypeScript types<\/strong><br \/> \u2705 Define content in JSON, JS, or TS<br \/> \u2705 <strong>Embed external files<\/strong> (Markdown, TXT&#8230;)<br \/> \u2705 Fetch and type remote content instantly<br \/> \u2705 Native CMS compatibility for editing content externally<\/p>\n<h3>\u26a1 Getting Started (React Native)<\/h3>\n<h4>1\ufe0f\u20e3 Install<\/h4>\n<pre><code class=\"bash\">npm install intlayer react-intlayer react-native-intlayer<\/code><\/pre>\n<h4>2\ufe0f\u20e3 Configure Locales<\/h4>\n<p>Create <code>intlayer.config.ts<\/code> at the root:<\/p>\n<pre><code>import { Locales, type IntlayerConfig } from \"intlayer\";  const config: IntlayerConfig = {   internationalization: {     locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],     defaultLocale: Locales.ENGLISH,   }, };  export default config;<\/code><\/pre>\n<h4>3\ufe0f\u20e3 Add Metro Support<\/h4>\n<p>In <code>metro.config.js<\/code>:<\/p>\n<pre><code>const { getDefaultConfig } = require(\"expo\/metro-config\"); const { configMetroIntlayer } = require(\"react-native-intlayer\/metro\");  module.exports = (async () =&gt; {   const defaultConfig = getDefaultConfig(__dirname);   return await configMetroIntlayer(defaultConfig); })();<\/code><\/pre>\n<h4>4\ufe0f\u20e3 Wrap Your App<\/h4>\n<p>In <code>_layout.tsx<\/code>:<\/p>\n<pre><code>import { Stack } from \"expo-router\"; import { getLocales } from \"expo-localization\"; import { IntlayerProviderContent } from \"react-intlayer\"; import { intlayerPolyfill } from \"react-native-intlayer\";  intlayerPolyfill();  const RootLayout = () =&gt; {   return (     &lt;IntlayerProviderContent defaultLocale={getLocales()[0]?.languageTag}&gt;       &lt;Stack&gt;         &lt;Stack.Screen name=\"(tabs)\" \/&gt;       &lt;\/Stack&gt;     &lt;\/IntlayerProviderContent&gt;   ); };  export default RootLayout;<\/code><\/pre>\n<h4>5\ufe0f\u20e3 Define Localized Content by Component<\/h4>\n<p>Keep translations close to your UI:<\/p>\n<pre><code>import { t, md, file, type Dictionary } from \"intlayer\";  const homeScreenContent = {   key: \"home-screen\",   content: {     title: t({       en: \"My Title\",       fr: \"Mon titre\",       es: \"Mi t\u00edtulo\",     }),     description: t({       en: md(file(\".\/myDescription.en.md\")),       fr: md(file(\".\/myDescription.fr.md\")),       es: md(file(\".\/myDescription.es.md\")),     }),     contentFetch: fetch(\"https:\/\/example.com\").then((res) =&gt; res.text()),   }, } satisfies Dictionary;  export default homeScreenContent;<\/code><\/pre>\n<p>Then use them like this:<\/p>\n<pre><code>import { Text, View } from \"react-native\"; import { useIntlayer } from \"react-intlayer\";  const MyComponent = () =&gt; {   const { title, description, contentFetch } = useIntlayer(\"my-component\");    return (     &lt;View&gt;       &lt;Text&gt;{title}&lt;\/Text&gt;       &lt;Text&gt;{description}&lt;\/Text&gt;       &lt;Text&gt;{contentFetch}&lt;\/Text&gt;     &lt;\/View&gt;   ); }; <\/code><\/pre>\n<h3>\ud83d\udd04 Switch Languages at Runtime<\/h3>\n<pre><code>import { View, Text, TouchableOpacity } from \"react-native\"; import { getLocaleName } from \"intlayer\"; import { useLocale } from \"react-intlayer\";  const LocaleSwitcher = () =&gt; {   const { setLocale, availableLocales } = useLocale();    return (     &lt;View&gt;       {availableLocales.map((locale) =&gt; (         &lt;TouchableOpacity key={locale} onPress={() =&gt; setLocale(locale)}&gt;           &lt;Text&gt;{getLocaleName(locale)}&lt;\/Text&gt;         &lt;\/TouchableOpacity&gt;       ))}     &lt;\/View&gt;   ); };  export default LocaleSwitcher;<\/code><\/pre>\n<h3>\ud83d\udcda Resources<\/h3>\n<ul>\n<li>\n<p>\u2b50 <a href=\"https:\/\/github.com\/aymericzip\/intlayer\" rel=\"noopener noreferrer nofollow\">GitHub Issues &amp; Feedback<\/a><\/p>\n<\/li>\n<li>\n<p>\ud83d\udcc4 <a href=\"https:\/\/intlayer.org\/doc\/environment\/react-native-and-expo\" rel=\"noopener noreferrer nofollow\">React Native &amp; Expo Docs<\/a><\/p>\n<\/li>\n<li>\n<p>\ud83d\udd39 <a href=\"https:\/\/github.com\/aymericzip\/intlayer-react-native-template\" rel=\"noopener noreferrer nofollow\">React Native Template<\/a><\/p>\n<\/li>\n<\/ul>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/893170\/\"> https:\/\/habr.com\/ru\/articles\/893170\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>If you\u2019re building a multilingual React Native (or web) app, you\u2019ve probably tried react-i18next, i18n-js, LinguiJS, or similar libraries.<\/p>\n<p>But in every project, the same issues come up:<\/p>\n<p> \u274c Unused key-value pairs are never removed<br \/> \u274c Content gets duplicated<br \/> \u274c Ensuring format consistency across languages is painful<br \/> \u274c i18next doesn\u2019t generate TypeScript types by default \u2013 so t(&#171;my.key&#187;) won\u2019t throw even if it\u2019s been deleted<br \/> \u274c Localization platforms like Lokalise or Locize get expensive fast<\/p>\n<p>Frustrated by these challenges, I waited for a better solution&#8230; then decided to build one myself: <a href=\"https:\/\/intlayer.org\/\" rel=\"noopener noreferrer nofollow\"><strong>Intlayer<\/strong><\/a>.<\/p>\n<h3>\u2728 Key points<\/h3>\n<p> \u2705 Works with <strong>React Native<\/strong> and <strong>Lynx<\/strong><br \/> \u2705 Easy integration<br \/> \u2705 <strong>Localized content close to your components<\/strong><br \/> \u2705 <strong>Autogenerated TypeScript types<\/strong><br \/> \u2705 Define content in JSON, JS, or TS<br \/> \u2705 <strong>Embed external files<\/strong> (Markdown, TXT&#8230;)<br \/> \u2705 Fetch and type remote content instantly<br \/> \u2705 Native CMS compatibility for editing content externally<\/p>\n<h3>\u26a1 Getting Started (React Native)<\/h3>\n<h4>1\ufe0f\u20e3 Install<\/h4>\n<pre><code class=\"bash\">npm install intlayer react-intlayer react-native-intlayer<\/code><\/pre>\n<h4>2\ufe0f\u20e3 Configure Locales<\/h4>\n<p>Create <code>intlayer.config.ts<\/code> at the root:<\/p>\n<pre><code>import { Locales, type IntlayerConfig } from \"intlayer\";  const config: IntlayerConfig = {   internationalization: {     locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],     defaultLocale: Locales.ENGLISH,   }, };  export default config;<\/code><\/pre>\n<h4>3\ufe0f\u20e3 Add Metro Support<\/h4>\n<p>In <code>metro.config.js<\/code>:<\/p>\n<pre><code>const { getDefaultConfig } = require(\"expo\/metro-config\"); const { configMetroIntlayer } = require(\"react-native-intlayer\/metro\");  module.exports = (async () =&gt; {   const defaultConfig = getDefaultConfig(__dirname);   return await configMetroIntlayer(defaultConfig); })();<\/code><\/pre>\n<h4>4\ufe0f\u20e3 Wrap Your App<\/h4>\n<p>In <code>_layout.tsx<\/code>:<\/p>\n<pre><code>import { Stack } from \"expo-router\"; import { getLocales } from \"expo-localization\"; import { IntlayerProviderContent } from \"react-intlayer\"; import { intlayerPolyfill } from \"react-native-intlayer\";  intlayerPolyfill();  const RootLayout = () =&gt; {   return (     &lt;IntlayerProviderContent defaultLocale={getLocales()[0]?.languageTag}&gt;       &lt;Stack&gt;         &lt;Stack.Screen name=\"(tabs)\" \/&gt;       &lt;\/Stack&gt;     &lt;\/IntlayerProviderContent&gt;   ); };  export default RootLayout;<\/code><\/pre>\n<h4>5\ufe0f\u20e3 Define Localized Content by Component<\/h4>\n<p>Keep translations close to your UI:<\/p>\n<pre><code>import { t, md, file, type Dictionary } from \"intlayer\";  const homeScreenContent = {   key: \"home-screen\",   content: {     title: t({       en: \"My Title\",       fr: \"Mon titre\",       es: \"Mi t\u00edtulo\",     }),     description: t({       en: md(file(\".\/myDescription.en.md\")),       fr: md(file(\".\/myDescription.fr.md\")),       es: md(file(\".\/myDescription.es.md\")),     }),     contentFetch: fetch(\"https:\/\/example.com\").then((res) =&gt; res.text()),   }, } satisfies Dictionary;  export default homeScreenContent;<\/code><\/pre>\n<p>Then use them like this:<\/p>\n<pre><code>import { Text, View } from \"react-native\"; import { useIntlayer } from \"react-intlayer\";  const MyComponent = () =&gt; {   const { title, description, contentFetch } = useIntlayer(\"my-component\");    return (     &lt;View&gt;       &lt;Text&gt;{title}&lt;\/Text&gt;       &lt;Text&gt;{description}&lt;\/Text&gt;       &lt;Text&gt;{contentFetch}&lt;\/Text&gt;     &lt;\/View&gt;   ); }; <\/code><\/pre>\n<h3>\ud83d\udd04 Switch Languages at Runtime<\/h3>\n<pre><code>import { View, Text, TouchableOpacity } from \"react-native\"; import { getLocaleName } from \"intlayer\"; import { useLocale } from \"react-intlayer\";  const LocaleSwitcher = () =&gt; {   const { setLocale, availableLocales } = useLocale();    return (     &lt;View&gt;       {availableLocales.map((locale) =&gt; (         &lt;TouchableOpacity key={locale} onPress={() =&gt; setLocale(locale)}&gt;           &lt;Text&gt;{getLocaleName(locale)}&lt;\/Text&gt;         &lt;\/TouchableOpacity&gt;       ))}     &lt;\/View&gt;   ); };  export default LocaleSwitcher;<\/code><\/pre>\n<h3>\ud83d\udcda Resources<\/h3>\n<ul>\n<li>\n<p>\u2b50 <a href=\"https:\/\/github.com\/aymericzip\/intlayer\" rel=\"noopener noreferrer nofollow\">GitHub Issues &amp; Feedback<\/a><\/p>\n<\/li>\n<li>\n<p>\ud83d\udcc4 <a href=\"https:\/\/intlayer.org\/doc\/environment\/react-native-and-expo\" rel=\"noopener noreferrer nofollow\">React Native &amp; Expo Docs<\/a><\/p>\n<\/li>\n<li>\n<p>\ud83d\udd39 <a href=\"https:\/\/github.com\/aymericzip\/intlayer-react-native-template\" rel=\"noopener noreferrer nofollow\">React Native Template<\/a><\/p>\n<\/li>\n<\/ul>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/893170\/\"> https:\/\/habr.com\/ru\/articles\/893170\/<\/a><br \/><\/br><\/br><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-452698","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/452698","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=452698"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/452698\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=452698"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=452698"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=452698"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}