diff --git a/app/components/AccountDetails.tsx b/app/components/AccountDetails.tsx
index ce757b9318..57a95a57b7 100644
--- a/app/components/AccountDetails.tsx
+++ b/app/components/AccountDetails.tsx
@@ -1,6 +1,6 @@
-import { Link, Outlet, useOutlet } from "@remix-run/react";
+import { Outlet, useOutlet } from "@remix-run/react";
import type { Customer } from "@shopify/hydrogen-ui-alpha/storefront-api-types";
-import { Modal } from "~/components";
+import { Modal, LinkI18n } from "~/components";
import type { AccountDetailsOutletContext } from "~/routes/account/edit";
export function AccountDetails({ customer }: { customer: Customer }) {
@@ -20,9 +20,9 @@ export function AccountDetails({ customer }: { customer: Customer }) {
Profile & Security
-
+
Edit
-
+
Name
diff --git a/app/components/CartDetails.tsx b/app/components/CartDetails.tsx
index 1fdfa25f68..4bf90e6c4c 100644
--- a/app/components/CartDetails.tsx
+++ b/app/components/CartDetails.tsx
@@ -3,9 +3,9 @@ import { useScroll } from "react-use";
import { flattenConnection, Money } from "@shopify/hydrogen-ui-alpha";
import {
type FetcherWithComponents,
- Link,
useFetcher,
useLocation,
+ Link,
} from "@remix-run/react";
import {
@@ -15,6 +15,7 @@ import {
ProductCard,
Skeleton,
Text,
+ LinkI18n,
} from "~/components";
import type {
Cart,
@@ -185,9 +186,9 @@ function CartLineItem({
-
+
{merchandise.product.title}
-
+
diff --git a/app/components/CountrySelector.tsx b/app/components/CountrySelector.tsx
index 17b0688888..ac39744617 100644
--- a/app/components/CountrySelector.tsx
+++ b/app/components/CountrySelector.tsx
@@ -1,25 +1,20 @@
-import { Link, useAsyncValue, useLocation, Await } from "@remix-run/react";
+import { Link, useAsyncValue, useLocation, Await, useParams } from "@remix-run/react";
import { Country } from "@shopify/hydrogen-ui-alpha/storefront-api-types";
import {Listbox} from '@headlessui/react';
import { useState, Suspense } from "react";
import { IconCaret, IconCheck } from "./Icon";
+import { getLocalizationFromLang } from "~/lib/utils";
export function CountrySelector({
- defaultCountry,
countries,
}: {
- defaultCountry: Country;
countries: Array
;
}) {
- const selectedCountry = defaultCountry.isoCode;
return (
}>
-
+
)
@@ -32,7 +27,7 @@ function CountrySelectorFallback() {
- --
+ --
@@ -40,17 +35,15 @@ function CountrySelectorFallback() {
)
}
-function CountrySelectorElement({
- defaultCountry,
- selectedCountry
-}: {
- defaultCountry: Country;
- selectedCountry: string;
-}) {
+function CountrySelectorElement() {
const [listboxOpen, setListboxOpen] = useState(false);
const countries = useAsyncValue>();
const { pathname } = useLocation();
- const currentCountry = countries.find(country => country.isoCode === selectedCountry) || defaultCountry;
+ const { lang } = useParams();
+ const { language, country } = getLocalizationFromLang(lang);
+ const languageIsoCode = language.toLowerCase();
+ const strippedPathname = pathname.replace(new RegExp(`^\/${lang}\/`), '/')
+ const currentCountry = countries.find(c => c.isoCode === country);
return (
@@ -64,7 +57,11 @@ function CountrySelectorElement({
open ? 'rounded-b md:rounded-t md:rounded-b-none' : 'rounded'
} border-contrast/30 dark:border-white`}
>
- {currentCountry.name} ({currentCountry.currency.isoCode} {currentCountry.currency.symbol})
+ {
+ currentCountry ?
+ `${currentCountry.name} (${currentCountry.currency.isoCode} ${currentCountry.currency.symbol})` :
+ '--'
+ }
@@ -77,13 +74,13 @@ function CountrySelectorElement({
}`}
>
{listboxOpen && Object.values(countries).map(country => {
- const isSelected = country.isoCode === currentCountry.isoCode;
- const countryIsoCode = country.isoCode.toLocaleLowerCase();
+ const isSelected = country.isoCode === currentCountry?.isoCode;
+ const countryIsoCode = country.isoCode.toLowerCase();
return (
{({active}) => (
;
- defaultCountry: Country;
cart: Promise;
};
}) {
- const { layout, countries, defaultCountry, cart } = data || {};
+ const { layout, countries, cart } = data || {};
return (
<>
@@ -60,7 +61,6 @@ export function Layout({
>
);
@@ -75,12 +75,9 @@ function Header({
menu?: EnhancedMenu;
cart?: Promise;
}) {
- const { pathname } = useLocation();
-
- // TODO: Ensure locale support like in Hydrogen
- const isHome = pathname === "/";
- const localeMatch = /^\/([a-z]{2})(\/|$)/i.exec(pathname);
- const countryCode = localeMatch ? localeMatch[1] : null;
+ const { lang } = useParams();
+ const { country } = getLocalizationFromLang(lang);
+ const isHome = isHomePath();
const {
isOpen: isCartOpen,
@@ -101,7 +98,6 @@ function Header({
)}
;
- defaultCountry?: Country;
}) {
- const { pathname } = useLocation();
-
- // TODO: Ensure locale support like in Hydrogen
- const localeMatch = /^\/([a-z]{2})(\/|$)/i.exec(pathname);
- const countryCode = localeMatch ? localeMatch[1] : null;
-
- const isHome = pathname === `/${countryCode ? countryCode + "/" : ""}`;
+ const isHome = isHomePath();
const itemsCount = menu
? menu?.items?.length + 1 > 4
? 4
@@ -152,14 +139,13 @@ function Footer({
bg-primary dark:bg-contrast dark:text-primary text-contrast overflow-hidden`}
>
- {countries && defaultCountry && (
+ {countries && (
)}
@@ -256,25 +242,23 @@ function MenuMobileNav({
);
}
function MobileHeader({
- countryCode,
title,
isHome,
openCart,
openMenu,
cart,
}: {
- countryCode?: string | null;
title: string;
isHome: boolean;
openCart: () => void;
@@ -301,7 +285,7 @@ function MobileHeader({
-
{title}
-
+
-
+
-
+
-
+
-
View Details
-
+
);
diff --git a/app/components/ProductCard.tsx b/app/components/ProductCard.tsx
index 795bfd4f6e..df7c6c2e2b 100644
--- a/app/components/ProductCard.tsx
+++ b/app/components/ProductCard.tsx
@@ -6,7 +6,7 @@ import {
useMoney,
} from "@shopify/hydrogen-ui-alpha";
-import { Text } from "~/components";
+import { Text, LinkI18n } from "~/components";
import { isDiscounted, isNewArrival } from "~/lib/utils";
import { getProductPlaceholder } from "~/lib/placeholders";
import type {
@@ -15,7 +15,6 @@ import type {
ProductVariant,
ProductVariantConnection,
} from "@shopify/hydrogen-ui-alpha/storefront-api-types";
-import { Link } from "@remix-run/react";
export function ProductCard({
product,
@@ -53,7 +52,7 @@ export function ProductCard({
const styles = clsx("grid gap-6", className);
return (
-
-
+
);
}
diff --git a/app/components/ProductGrid.tsx b/app/components/ProductGrid.tsx
index be2b4f3c3f..b3f6c52420 100644
--- a/app/components/ProductGrid.tsx
+++ b/app/components/ProductGrid.tsx
@@ -1,10 +1,10 @@
-import { Button, Grid, ProductCard } from "~/components";
+import { Button, Grid, ProductCard, LinkI18n } from "~/components";
import { getImageLoadingPriority } from "~/lib/const";
import type {
Collection,
Product,
} from "@shopify/hydrogen-ui-alpha/storefront-api-types";
-import { Link, useFetcher } from "@remix-run/react";
+import { useFetcher } from "@remix-run/react";
import { useEffect, useState } from "react";
export function ProductGrid({
@@ -43,9 +43,9 @@ export function ProductGrid({
return (
<>
No products found on this collection
-
+
Browse catalog
-
+
>
);
}
diff --git a/app/components/index.ts b/app/components/index.ts
index bad412b063..5f660f33f2 100644
--- a/app/components/index.ts
+++ b/app/components/index.ts
@@ -14,5 +14,6 @@ export { CartDetails, CartEmpty } from "./CartDetails";
export { OrderCard } from "./OrderCard";
export { AccountDetails } from "./AccountDetails";
export { Modal } from "./Modal";
+export { LinkI18n } from './LinkI18n';
// Sue me
export * from "./Icon";
diff --git a/app/data/index.ts b/app/data/index.ts
index 10c3300a97..030fb3a62c 100644
--- a/app/data/index.ts
+++ b/app/data/index.ts
@@ -10,7 +10,6 @@ import type {
ProductConnection,
ProductVariant,
SelectedOptionInput,
- LanguageCode,
Blog,
PageConnection,
Shop,
@@ -27,10 +26,11 @@ import {
getPublicTokenHeaders,
getStorefrontApiUrl,
} from "~/lib/shopify-client";
-import { type EnhancedMenu, parseMenu, getApiErrorMessage } from "~/lib/utils";
+import { type EnhancedMenu, parseMenu, getApiErrorMessage, getLocalizationFromLang } from "~/lib/utils";
import invariant from "tiny-invariant";
import { logout } from "~/routes/account.logout";
import type { AppLoadContext } from "@remix-run/cloudflare";
+import { type Params } from "@remix-run/react";
type StorefrontApiResponse
= StorefrontApiResponseOk;
@@ -78,8 +78,8 @@ export interface LayoutData {
cart?: Promise;
}
-export async function getLayoutData() {
- const languageCode = "EN";
+export async function getLayoutData(params: Params) {
+ const { language } = getLocalizationFromLang(params.lang);
const HEADER_MENU_HANDLE = "main-menu";
const FOOTER_MENU_HANDLE = "footer";
@@ -87,7 +87,7 @@ export async function getLayoutData() {
const { data } = await getStorefrontData({
query: LAYOUT_QUERY,
variables: {
- language: languageCode,
+ language: language,
headerMenuHandle: HEADER_MENU_HANDLE,
footerMenuHandle: FOOTER_MENU_HANDLE,
},
@@ -188,11 +188,10 @@ const COUNTRIES_QUERY = `#graphql
export async function getProductData(
handle: string,
- searchParams: URLSearchParams
+ searchParams: URLSearchParams,
+ params: Params
) {
- // TODO: Figure out localization stuff
- const languageCode = "EN";
- const countryCode = "US";
+ const { language, country } = getLocalizationFromLang(params.lang);
let selectedOptions: SelectedOptionInput[] = [];
searchParams.forEach((value, name) => {
@@ -205,8 +204,8 @@ export async function getProductData(
}>({
query: PRODUCT_QUERY,
variables: {
- country: countryCode,
- language: languageCode,
+ country,
+ language,
selectedOptions,
handle,
},
@@ -374,9 +373,9 @@ const RECOMMENDED_PRODUCTS_QUERY = `#graphql
query productRecommendations(
$productId: ID!
$count: Int
- $countryCode: CountryCode
- $languageCode: LanguageCode
- ) @inContext(country: $countryCode, language: $languageCode) {
+ $country: CountryCode
+ $language: LanguageCode
+ ) @inContext(country: $country, language: $language) {
recommended: productRecommendations(productId: $productId) {
...ProductCard
}
@@ -388,11 +387,12 @@ const RECOMMENDED_PRODUCTS_QUERY = `#graphql
}
`;
-export async function getRecommendedProducts(productId: string, count = 12) {
- // TODO: You know what to do
- const languageCode = "EN";
- const countryCode = "US";
-
+export async function getRecommendedProducts(
+ productId: string,
+ params: Params,
+ count = 12
+) {
+ const { language, country } = getLocalizationFromLang(params.lang);
const { data: products } = await getStorefrontData<{
recommended: Product[];
additional: ProductConnection;
@@ -401,8 +401,8 @@ export async function getRecommendedProducts(productId: string, count = 12) {
variables: {
productId,
count,
- language: languageCode,
- country: countryCode,
+ language,
+ country,
},
});
@@ -453,11 +453,10 @@ const COLLECTIONS_QUERY = `#graphql
`;
export async function getCollections(
+ params: Params,
{ paginationSize } = { paginationSize: 8 }
) {
- // TODO: You know what to do
- const languageCode = "EN";
- const countryCode = "US";
+ const { language, country } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{
collections: CollectionConnection;
@@ -465,8 +464,8 @@ export async function getCollections(
query: COLLECTIONS_QUERY,
variables: {
pageBy: paginationSize,
- country: countryCode,
- language: languageCode,
+ country,
+ language,
},
});
@@ -517,14 +516,14 @@ export async function getCollection({
handle,
paginationSize = 48,
cursor,
+ params,
}: {
handle: string;
paginationSize?: number;
cursor?: string;
+ params: Params;
}) {
- // TODO: You know what to do
- const languageCode = "EN";
- const countryCode = "US";
+ const { language, country } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{
collection: Collection;
@@ -533,8 +532,8 @@ export async function getCollection({
variables: {
handle,
cursor,
- language: languageCode,
- country: countryCode,
+ language,
+ country,
pageBy: paginationSize,
},
});
@@ -572,13 +571,13 @@ const ALL_PRODUCTS_QUERY = `#graphql
export async function getAllProducts({
paginationSize = 48,
cursor,
+ params,
}: {
paginationSize?: number;
cursor?: string;
+ params: Params;
}) {
- // TODO: You know what to do
- const languageCode = "EN";
- const countryCode = "US";
+ const { language, country } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{
products: ProductConnection;
@@ -586,8 +585,8 @@ export async function getAllProducts({
query: ALL_PRODUCTS_QUERY,
variables: {
cursor,
- language: languageCode,
- country: countryCode,
+ language,
+ country,
pageBy: paginationSize,
},
});
@@ -709,9 +708,8 @@ mutation CartCreate($input: CartInput!, $country: CountryCode = ZZ) @inContext(c
}
`;
-export async function createCart({ cart }: { cart: CartInput }) {
- // TODO: You know what to do
- const countryCode = "US";
+export async function createCart({ cart, params }: { cart: CartInput, params: Params }) {
+ const { country } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{
cartCreate: {
@@ -721,7 +719,7 @@ export async function createCart({ cart }: { cart: CartInput }) {
query: CREATE_CART_MUTATION,
variables: {
input: cart,
- country: countryCode,
+ country,
},
});
@@ -743,12 +741,13 @@ const ADD_LINE_ITEM_QUERY = `#graphql
export async function addLineItem({
cartId,
lines,
+ params
}: {
cartId: string;
lines: CartLineInput[];
+ params: Params;
}) {
- // TODO: You know what to do
- const countryCode = "US";
+ const { country } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{
cartLinesAdd: {
@@ -756,7 +755,7 @@ export async function addLineItem({
};
}>({
query: ADD_LINE_ITEM_QUERY,
- variables: { cartId, lines, country: countryCode },
+ variables: { cartId, lines, country },
});
invariant(data, "No data returned from Shopify API");
@@ -774,15 +773,17 @@ const CART_QUERY = `#graphql
${CART_FRAGMENT}
`;
-export async function getCart({ cartId }: { cartId: string }) {
- // TODO: Yes
- const countryCode = "US";
+export async function getCart({ cartId, params }: {
+ cartId: string;
+ params: Params;
+}) {
+ const { country } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{ cart: Cart }>({
query: CART_QUERY,
variables: {
cartId,
- country: countryCode,
+ country,
},
});
@@ -806,11 +807,13 @@ const UPDATE_LINE_ITEM_QUERY = `#graphql
export async function updateLineItem({
cartId,
lineItem,
+ params,
}: {
cartId: string;
lineItem: CartLineUpdateInput;
+ params: Params;
}) {
- const countryCode = "US";
+ const { country } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{ cartLinesUpdate: { cart: Cart } }>(
{
@@ -818,7 +821,7 @@ export async function updateLineItem({
variables: {
cartId,
lines: [lineItem],
- country: countryCode,
+ country,
},
}
);
@@ -832,9 +835,9 @@ const TOP_PRODUCTS_QUERY = `#graphql
${PRODUCT_CARD_FRAGMENT}
query topProducts(
$count: Int
- $countryCode: CountryCode
- $languageCode: LanguageCode
- ) @inContext(country: $countryCode, language: $languageCode) {
+ $country: CountryCode
+ $language: LanguageCode
+ ) @inContext(country: $country, language: $language) {
products(first: $count, sortKey: BEST_SELLING) {
nodes {
...ProductCard
@@ -843,9 +846,11 @@ const TOP_PRODUCTS_QUERY = `#graphql
}
`;
-export async function getTopProducts({ count = 4 }: { count?: number } = {}) {
- const countryCode = "US";
- const languageCode = "EN";
+export async function getTopProducts({ params, count = 4 }: {
+ params: Params;
+ count?: number
+}) {
+ const { language, country } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{
products: ProductConnection;
@@ -853,8 +858,8 @@ export async function getTopProducts({ count = 4 }: { count?: number } = {}) {
query: TOP_PRODUCTS_QUERY,
variables: {
count,
- countryCode,
- languageCode,
+ country,
+ language,
},
});
@@ -913,13 +918,20 @@ interface SitemapQueryData {
pages: PageConnection;
}
-export async function getSitemap(variables: {
- language: string;
+export async function getSitemap({
+ params,
+ urlLimits,
+}: {
+ params: Params;
urlLimits: number;
}) {
+ const { language } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData({
query: SITEMAP_QUERY,
- variables,
+ variables: {
+ urlLimits,
+ language,
+ },
});
invariant(data, "Sitemap data is missing");
@@ -961,14 +973,15 @@ query Blog(
`;
export async function getBlog({
- language,
+ params,
paginationSize,
blogHandle,
}: {
- language: LanguageCode;
+ params: Params;
blogHandle: string;
paginationSize: number;
}) {
+ const { language } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{
blog: Blog;
}>({
@@ -1015,16 +1028,25 @@ const ARTICLE_QUERY = `#graphql
}
`;
-export async function getArticle(variables: {
- language: LanguageCode;
+export async function getArticle({
+ params,
+ blogHandle,
+ articleHandle,
+}: {
+ params: Params;
blogHandle: string;
articleHandle: string;
}) {
+ const { language } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{
blog: Blog;
}>({
query: ARTICLE_QUERY,
- variables,
+ variables: {
+ blogHandle,
+ articleHandle,
+ language
+ },
});
invariant(data, "No data returned from Shopify API");
@@ -1037,8 +1059,8 @@ export async function getArticle(variables: {
}
const PAGE_QUERY = `#graphql
- query PageDetails($languageCode: LanguageCode, $handle: String!)
- @inContext(language: $languageCode) {
+ query PageDetails($language: LanguageCode, $handle: String!)
+ @inContext(language: $language) {
page(handle: $handle) {
id
title
@@ -1051,13 +1073,20 @@ const PAGE_QUERY = `#graphql
}
`;
-export async function getPageData(variables: {
- language: LanguageCode;
+export async function getPageData({
+ params,
+ handle,
+}: {
+ params: Params;
handle: string;
}) {
+ const { language } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{ page: Page }>({
query: PAGE_QUERY,
- variables,
+ variables: {
+ language,
+ handle
+ },
});
invariant(data, "No data returned from Shopify API");
@@ -1093,16 +1122,19 @@ const NOT_FOUND_QUERY = `#graphql
}
`;
-export async function getFeaturedData(variables: {
- language: LanguageCode;
- country: CountryCode;
+export async function getFeaturedData({params}: {
+ params: Params;
}) {
+ const { language, country } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{
featuredCollections: CollectionConnection;
featuredProducts: ProductConnection;
}>({
query: NOT_FOUND_QUERY,
- variables,
+ variables: {
+ language,
+ country,
+ },
});
return data;
@@ -1254,13 +1286,14 @@ export async function getCustomer({
request,
context,
customerAccessToken,
+ params,
}: {
request: Request;
context: AppLoadContext;
customerAccessToken: string;
+ params: Params;
}) {
- const countryCode = "US";
- const languageCode = "EN";
+ const { language, country } = getLocalizationFromLang(params.lang);
const { data } = await getStorefrontData<{
customer: Customer;
@@ -1268,8 +1301,8 @@ export async function getCustomer({
query: CUSTOMER_QUERY,
variables: {
customerAccessToken,
- country: countryCode,
- language: languageCode,
+ country,
+ language,
},
});
diff --git a/app/lib/utils.ts b/app/lib/utils.ts
index 50b60d2878..c3b7824ff9 100644
--- a/app/lib/utils.ts
+++ b/app/lib/utils.ts
@@ -1,8 +1,11 @@
+import { useLocation, useParams, type Params } from "@remix-run/react";
import type {
MenuItem,
Menu,
MoneyV2,
UserError,
+ CountryCode,
+ LanguageCode,
} from "@shopify/hydrogen-ui-alpha/storefront-api-types";
// @ts-expect-error types not available
@@ -250,3 +253,29 @@ export function getApiErrorMessage(
return data[field].customerUserErrors[0].message;
return null;
}
+
+export function getLocalizationFromLang(lang?: String): {
+ language: LanguageCode,
+ country: CountryCode
+} {
+ if (lang) {
+ const [language, country] = lang.split('-');
+
+ return {
+ language: language.toUpperCase() as LanguageCode,
+ country: (country.toUpperCase() || 'US') as CountryCode,
+ }
+ }
+ return {
+ language: 'EN' as LanguageCode,
+ country: 'US' as CountryCode,
+ }
+}
+
+export function isHomePath() {
+ const { pathname } = useLocation();
+ const { lang } = useParams();
+ const strippedPathname = pathname.replace(new RegExp(`^\/${lang}\/`), '/')
+
+ return strippedPathname === '/';
+}
diff --git a/app/root.tsx b/app/root.tsx
index 1a10e1eda0..ee3b3946dc 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -47,22 +47,15 @@ export const meta: MetaFunction = () => ({
export const loader: LoaderFunction = async function loader({
request,
context,
+ params
}) {
const session = await getSession(request, context);
const cartId = await session.get("cartId");
return defer({
- layout: await getLayoutData(),
- defaultCountry: await {
- currency: {
- isoCode: "USD",
- symbol: "$",
- },
- isoCode: "US",
- name: "United States",
- },
+ layout: await getLayoutData(params),
countries: getCountries(),
- cart: cartId ? getCart({ cartId }) : undefined,
+ cart: cartId ? getCart({ cartId, params }) : undefined,
});
};
diff --git a/app/routes/$lang/[robots.txt].tsx b/app/routes/$lang/[robots.txt].tsx
new file mode 100644
index 0000000000..c4ce41ce3d
--- /dev/null
+++ b/app/routes/$lang/[robots.txt].tsx
@@ -0,0 +1 @@
+export { loader } from '~/routes/[robots.txt]';
diff --git a/app/routes/$lang/[sitemap.xml].tsx b/app/routes/$lang/[sitemap.xml].tsx
new file mode 100644
index 0000000000..4a1afbd015
--- /dev/null
+++ b/app/routes/$lang/[sitemap.xml].tsx
@@ -0,0 +1 @@
+export { loader } from '~/routes/[sitemap.xml]';
diff --git a/app/routes/$lang/account.login.tsx b/app/routes/$lang/account.login.tsx
new file mode 100644
index 0000000000..da16b3f5bd
--- /dev/null
+++ b/app/routes/$lang/account.login.tsx
@@ -0,0 +1,4 @@
+import Component, { loader, action } from '~/routes/account.login';
+
+export { loader, action };
+export default Component;
diff --git a/app/routes/$lang/account.logout.tsx b/app/routes/$lang/account.logout.tsx
new file mode 100644
index 0000000000..2bfccac9e5
--- /dev/null
+++ b/app/routes/$lang/account.logout.tsx
@@ -0,0 +1 @@
+export { logout, loader, action } from '~/routes/account.logout';
diff --git a/app/routes/$lang/account.tsx b/app/routes/$lang/account.tsx
new file mode 100644
index 0000000000..0468a5ac0b
--- /dev/null
+++ b/app/routes/$lang/account.tsx
@@ -0,0 +1,4 @@
+import Component, { loader } from '~/routes/account';
+
+export { loader };
+export default Component;
diff --git a/app/routes/$lang/account/edit.tsx b/app/routes/$lang/account/edit.tsx
new file mode 100644
index 0000000000..f41c1ecf9d
--- /dev/null
+++ b/app/routes/$lang/account/edit.tsx
@@ -0,0 +1,4 @@
+import Component, { action } from '~/routes/account/edit';
+
+export { action };
+export default Component;
diff --git a/app/routes/$lang/cart.tsx b/app/routes/$lang/cart.tsx
new file mode 100644
index 0000000000..2e66254c82
--- /dev/null
+++ b/app/routes/$lang/cart.tsx
@@ -0,0 +1,4 @@
+import Component, { loader, action } from '~/routes/cart';
+
+export { loader, action };
+export default Component;
diff --git a/app/routes/$lang/collections/$collectionHandle.tsx b/app/routes/$lang/collections/$collectionHandle.tsx
new file mode 100644
index 0000000000..cf02ccbf86
--- /dev/null
+++ b/app/routes/$lang/collections/$collectionHandle.tsx
@@ -0,0 +1,4 @@
+import Component, { loader, meta } from '~/routes/collections/$collectionHandle';
+
+export { loader, meta };
+export default Component;
diff --git a/app/routes/$lang/collections/all.tsx b/app/routes/$lang/collections/all.tsx
new file mode 100644
index 0000000000..fc179d414e
--- /dev/null
+++ b/app/routes/$lang/collections/all.tsx
@@ -0,0 +1 @@
+export { loader } from '~/routes/collections/all';
diff --git a/app/routes/$lang/collections/index.tsx b/app/routes/$lang/collections/index.tsx
new file mode 100644
index 0000000000..54a4f9448a
--- /dev/null
+++ b/app/routes/$lang/collections/index.tsx
@@ -0,0 +1,4 @@
+import Component, { loader, meta } from '~/routes/collections/index';
+
+export { loader, meta };
+export default Component;
diff --git a/app/routes/$lang/index.tsx b/app/routes/$lang/index.tsx
new file mode 100644
index 0000000000..114bf5d14d
--- /dev/null
+++ b/app/routes/$lang/index.tsx
@@ -0,0 +1,7 @@
+import Component from '~/routes/index';
+
+export default Component;
+
+// Everything in the $lang folder is a re-export of the route folder
+// Wondering if we can just have it as a build process (dynamically creates these
+// files based on the files in the routes folder) during build/hot reload process
diff --git a/app/routes/$lang/journal/$journalHandle.tsx b/app/routes/$lang/journal/$journalHandle.tsx
new file mode 100644
index 0000000000..f92ace8742
--- /dev/null
+++ b/app/routes/$lang/journal/$journalHandle.tsx
@@ -0,0 +1,4 @@
+import Component, { loader, meta, links, CatchBoundary } from '~/routes/journal/$journalHandle';
+
+export { loader, meta, links, CatchBoundary };
+export default Component;
diff --git a/app/routes/$lang/journal/index.tsx b/app/routes/$lang/journal/index.tsx
new file mode 100644
index 0000000000..2754f4a262
--- /dev/null
+++ b/app/routes/$lang/journal/index.tsx
@@ -0,0 +1,4 @@
+import Component, { loader, meta } from '~/routes/journal/index';
+
+export { loader, meta };
+export default Component;
diff --git a/app/routes/$lang/products/$productHandle.tsx b/app/routes/$lang/products/$productHandle.tsx
new file mode 100644
index 0000000000..3b405d6c74
--- /dev/null
+++ b/app/routes/$lang/products/$productHandle.tsx
@@ -0,0 +1,4 @@
+import Component, { loader, action, ProductForm } from '~/routes/products/$productHandle';
+
+export { loader, action, ProductForm };
+export default Component;
diff --git a/app/routes/$lang/products/index.tsx b/app/routes/$lang/products/index.tsx
new file mode 100644
index 0000000000..d7886eba8e
--- /dev/null
+++ b/app/routes/$lang/products/index.tsx
@@ -0,0 +1,4 @@
+import Component, { loader, meta } from '~/routes/products/index';
+
+export { loader, meta };
+export default Component;
diff --git a/app/routes/[sitemap.xml].tsx b/app/routes/[sitemap.xml].tsx
index c2ac98a48e..9c23b88448 100644
--- a/app/routes/[sitemap.xml].tsx
+++ b/app/routes/[sitemap.xml].tsx
@@ -4,9 +4,9 @@ import { getSitemap } from "~/data";
const MAX_URLS = 250; // the google limit is 50K, however, SF API only allow querying for 250 resources each time
-export async function loader({ request }: LoaderArgs) {
+export async function loader({ request, params }: LoaderArgs) {
const data = await getSitemap({
- language: "EN",
+ params,
urlLimits: MAX_URLS,
});
diff --git a/app/routes/account.login.tsx b/app/routes/account.login.tsx
index 1ddd67a6ab..e1fca2d1ec 100644
--- a/app/routes/account.login.tsx
+++ b/app/routes/account.login.tsx
@@ -5,11 +5,12 @@ import {
type ActionFunction,
type LoaderArgs,
} from "@remix-run/cloudflare";
-import { Form, Link, useActionData, useLoaderData } from "@remix-run/react";
+import { Form, useActionData, useLoaderData } from "@remix-run/react";
import { useState } from "react";
import { login, StorefrontApiError } from "~/data";
import { getSession } from "~/lib/session.server";
import { getInputStyleClasses } from "~/lib/utils";
+import { LinkI18n } from '~/components';
export async function loader({ request, context }: LoaderArgs) {
const session = await getSession(request, context);
@@ -173,20 +174,20 @@ export default function Login() {
New to {shopName}?
-
+
Create an account
-
+
diff --git a/app/routes/account/edit.tsx b/app/routes/account/edit.tsx
index 5d192cfd8a..e0cba527da 100644
--- a/app/routes/account/edit.tsx
+++ b/app/routes/account/edit.tsx
@@ -43,7 +43,7 @@ const formDataHas = (formData: FormData, key: string) => {
return typeof value === "string" && value.length > 0;
};
-export const action: ActionFunction = async ({ request, context }) => {
+export const action: ActionFunction = async ({ request, context, params }) => {
const [formData, session] = await Promise.all([
request.formData(),
getSession(request, context),
@@ -58,7 +58,7 @@ export const action: ActionFunction = async ({ request, context }) => {
// Double-check current user is logged in.
// Will throw a logout redirect if not.
- await getCustomer({ customerAccessToken, request, context });
+ await getCustomer({ customerAccessToken, request, context, params });
if (
formDataHas(formData, "newPassword") &&
diff --git a/app/routes/cart.tsx b/app/routes/cart.tsx
index 81b8df00d1..548b0e86cf 100644
--- a/app/routes/cart.tsx
+++ b/app/routes/cart.tsx
@@ -1,15 +1,15 @@
-import { type ActionFunction, json, defer } from "@remix-run/cloudflare";
+import { type ActionFunction, json, defer, LoaderArgs } from "@remix-run/cloudflare";
import invariant from "tiny-invariant";
import { getTopProducts, updateLineItem } from "~/data";
import { getSession } from "~/lib/session.server";
-export async function loader() {
+export async function loader({params}: LoaderArgs) {
return defer({
- topProducts: getTopProducts(),
+ topProducts: getTopProducts({params}),
});
}
-export const action: ActionFunction = async ({ request, context }) => {
+export const action: ActionFunction = async ({ request, context, params }) => {
const [session, formData] = await Promise.all([
getSession(request, context),
new URLSearchParams(await request.text()),
@@ -30,7 +30,7 @@ export const action: ActionFunction = async ({ request, context }) => {
switch (intent) {
case "set-quantity": {
const quantity = Number(formData.get("quantity"));
- await updateLineItem({ cartId, lineItem: { id: lineId, quantity } });
+ await updateLineItem({ cartId, lineItem: { id: lineId, quantity }, params });
break;
}
@@ -39,7 +39,7 @@ export const action: ActionFunction = async ({ request, context }) => {
* We're re-using the same mutation as setting a quantity of 0,
* but theoretically we could use the `cartLinesRemove` mutation.
*/
- await updateLineItem({ cartId, lineItem: { id: lineId, quantity: 0 } });
+ await updateLineItem({ cartId, lineItem: { id: lineId, quantity: 0 }, params });
break;
}
}
diff --git a/app/routes/collections/$collectionHandle.tsx b/app/routes/collections/$collectionHandle.tsx
index 9542f5e265..48711d7210 100644
--- a/app/routes/collections/$collectionHandle.tsx
+++ b/app/routes/collections/$collectionHandle.tsx
@@ -17,7 +17,7 @@ export async function loader({ params, request }: LoaderArgs) {
invariant(collectionHandle, "Missing collectionHandle param");
const cursor = new URL(request.url).searchParams.get("cursor") ?? undefined;
- const collection = await getCollection({ handle: collectionHandle, cursor });
+ const collection = await getCollection({ handle: collectionHandle, cursor, params });
return json({ collection });
}
diff --git a/app/routes/collections/index.tsx b/app/routes/collections/index.tsx
index 9510d04061..81b52f8521 100644
--- a/app/routes/collections/index.tsx
+++ b/app/routes/collections/index.tsx
@@ -1,12 +1,12 @@
-import { json, type MetaFunction } from "@remix-run/cloudflare";
-import { Link, useLoaderData } from "@remix-run/react";
+import { json, LoaderArgs, type MetaFunction } from "@remix-run/cloudflare";
+import { useLoaderData } from "@remix-run/react";
import type { Collection } from "@shopify/hydrogen-ui-alpha/storefront-api-types";
-import { Grid, Heading, PageHeader, Section } from "~/components";
+import { Grid, Heading, PageHeader, Section, LinkI18n } from "~/components";
import { getCollections } from "~/data";
import { getImageLoadingPriority } from "~/lib/const";
-export const loader = async () => {
- const collections = await getCollections();
+export const loader = async ({ params }: LoaderArgs) => {
+ const collections = await getCollections(params);
return json({ collections });
};
@@ -46,7 +46,7 @@ function CollectionCard({
loading?: HTMLImageElement["loading"];
}) {
return (
-
+
{collection?.image && (
{collection.title}
-
+
);
}
diff --git a/app/routes/featured-products.tsx b/app/routes/featured-products.tsx
index 125b539df8..a75ffbacb9 100644
--- a/app/routes/featured-products.tsx
+++ b/app/routes/featured-products.tsx
@@ -1,6 +1,6 @@
-import { json } from "@remix-run/cloudflare";
+import { json, LoaderArgs } from "@remix-run/cloudflare";
import { getFeaturedData } from "~/data";
-export async function loader() {
- return json(await getFeaturedData({ language: "EN", country: "US" }));
+export async function loader({params}: LoaderArgs) {
+ return json(await getFeaturedData({ params }));
}
diff --git a/app/routes/journal/$journalHandle.tsx b/app/routes/journal/$journalHandle.tsx
index 93114be941..915868e4f7 100644
--- a/app/routes/journal/$journalHandle.tsx
+++ b/app/routes/journal/$journalHandle.tsx
@@ -11,25 +11,24 @@ import invariant from "tiny-invariant";
import { PageHeader, Section } from "~/components";
import { getArticle } from "~/data";
import { ATTR_LOADING_EAGER } from "~/lib/const";
+import { getLocalizationFromLang } from "~/lib/utils";
import styles from "../../styles/custom-font.css";
const BLOG_HANDLE = "journal";
-export async function loader({ request, params }: LoaderArgs) {
- // TODO figure out localization
- const languageCode = "EN";
- const countryCode = "US";
+export async function loader({ params }: LoaderArgs) {
+ const { language, country } = getLocalizationFromLang(params.lang);
invariant(params.journalHandle, "Missing journal handle");
const article = await getArticle({
blogHandle: BLOG_HANDLE,
articleHandle: params.journalHandle,
- language: languageCode,
+ params,
});
const formattedDate = new Intl.DateTimeFormat(
- `${languageCode}-${countryCode}`,
+ `${language}-${country}`,
{
year: "numeric",
month: "long",
diff --git a/app/routes/journal/index.tsx b/app/routes/journal/index.tsx
index 62a24baf8d..c26dd1f897 100644
--- a/app/routes/journal/index.tsx
+++ b/app/routes/journal/index.tsx
@@ -1,29 +1,28 @@
-import { json, type MetaFunction } from "@remix-run/cloudflare";
-import { Link, useLoaderData } from "@remix-run/react";
+import { json, LoaderArgs, type MetaFunction } from "@remix-run/cloudflare";
+import { useLoaderData } from "@remix-run/react";
import { flattenConnection, Image } from "@shopify/hydrogen-ui-alpha";
import type { Article } from "@shopify/hydrogen-ui-alpha/storefront-api-types";
-import { Grid, PageHeader, Section } from "~/components";
+import { Grid, PageHeader, Section, LinkI18n } from "~/components";
import { getBlog } from "~/data";
import { getImageLoadingPriority, PAGINATION_SIZE } from "~/lib/const";
+import { getLocalizationFromLang } from "~/lib/utils";
const BLOG_HANDLE = "Journal";
-export const loader = async () => {
- // TODO figure out localization
- const languageCode = "EN";
- const countryCode = "US";
-
+export const loader = async ({params}: LoaderArgs) => {
const journals = await getBlog({
- language: languageCode,
+ params,
blogHandle: BLOG_HANDLE,
paginationSize: PAGINATION_SIZE,
});
+ const { language, country } = getLocalizationFromLang(params.lang);
+
const articles = flattenConnection(journals).map((article) => {
const { publishedAt } = article;
return {
...article,
- publishedAt: new Intl.DateTimeFormat(`${languageCode}-${countryCode}`, {
+ publishedAt: new Intl.DateTimeFormat(`${language}-${country}`, {
year: "numeric",
month: "long",
day: "numeric",
@@ -80,7 +79,7 @@ function ArticleCard({
}) {
return (
-
+
{article.image && (
{article.title}
{article.publishedAt}
-
+
);
}
diff --git a/app/routes/pages/$pageHandle.tsx b/app/routes/pages/$pageHandle.tsx
index 12e957d297..92932bcb31 100644
--- a/app/routes/pages/$pageHandle.tsx
+++ b/app/routes/pages/$pageHandle.tsx
@@ -10,14 +10,11 @@ import { PageHeader } from "~/components";
import { getPageData } from "~/data";
export async function loader({ params }: LoaderArgs) {
- // TODO figure out localization
- const languageCode = "EN";
-
invariant(params.pageHandle, "Missing page handle");
const page = await getPageData({
handle: params.pageHandle,
- language: languageCode,
+ params,
});
return json(
diff --git a/app/routes/products/$productHandle.tsx b/app/routes/products/$productHandle.tsx
index d147eea10d..11e70bdf11 100644
--- a/app/routes/products/$productHandle.tsx
+++ b/app/routes/products/$productHandle.tsx
@@ -6,7 +6,6 @@ import {
redirect,
} from "@remix-run/cloudflare";
import {
- Link,
useLoaderData,
Await,
useSearchParams,
@@ -26,6 +25,7 @@ import {
Section,
Skeleton,
Text,
+ LinkI18n,
} from "~/components";
import {
addLineItem,
@@ -44,13 +44,14 @@ export const loader = async ({ params, request }: LoaderArgs) => {
const { shop, product } = await getProductData(
productHandle,
- new URL(request.url).searchParams
+ new URL(request.url).searchParams,
+ params
);
return defer({
product,
shop,
- recommended: getRecommendedProducts(product.id),
+ recommended: getRecommendedProducts(product.id, params),
});
};
@@ -75,6 +76,7 @@ export const action: ActionFunction = async ({ request, context, params }) => {
if (!cartId) {
const cart = await createCart({
cart: { lines: [{ merchandiseId: variantId }] },
+ params
});
session.set("cartId", cart.id);
@@ -84,6 +86,7 @@ export const action: ActionFunction = async ({ request, context, params }) => {
await addLineItem({
cartId,
lines: [{ merchandiseId: variantId }],
+ params,
});
}
@@ -380,7 +383,7 @@ function ProductOptionLink({
clonedSearchParams.set(optionName, optionValue);
return (
-
{children ?? optionValue}
-
+
);
}
@@ -428,12 +431,12 @@ function ProductDetail({
/>
{learnMore && (
-
Learn more
-
+
)}
diff --git a/app/routes/products/index.tsx b/app/routes/products/index.tsx
index 40b4bc62c8..d4a154390c 100644
--- a/app/routes/products/index.tsx
+++ b/app/routes/products/index.tsx
@@ -4,9 +4,9 @@ import type { Collection } from "@shopify/hydrogen-ui-alpha/storefront-api-types
import { PageHeader, Section, ProductGrid } from "~/components";
import { getAllProducts } from "~/data";
-export async function loader({ request }: LoaderArgs) {
+export async function loader({ request, params }: LoaderArgs) {
const cursor = new URL(request.url).searchParams.get("cursor") ?? undefined;
- const products = await getAllProducts({ cursor });
+ const products = await getAllProducts({ cursor, params });
return products;
}