Skip to content

Commit

Permalink
i18n(domains): validation and updated logic (#9099)
Browse files Browse the repository at this point in the history
  • Loading branch information
ematipico committed Nov 20, 2023
1 parent 7ab5137 commit 07d2f95
Show file tree
Hide file tree
Showing 10 changed files with 908 additions and 334 deletions.
31 changes: 29 additions & 2 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,33 @@ export interface AstroUserConfig {
*
*/
routingStrategy?: 'prefix-always' | 'prefix-other-locales';

/**
* @docs
* @kind h4
* @name experimental.i18n.domains
* @type {Record<string, string> }
* @default '{}'
* @version 3.6.0
* @description
*
* Maps a locale to a domain (or sub-domain). When a locale is mapped to a domain, all the URLs that belong to it will respond to `https://fr.example.com/blog` and not to `/fr/blog`.
*
* ```js
* export defualt defineConfig({
* experimental: {
* i18n: {
* defaultLocale: "en",
* locales: ["en", "fr", "pt-br", "es"],
* domains: {
* fr: "https://fr.example.com",
* }
* }
* }
* })
* ```
*/
domains?: Record<string, string>;
};
/**
* @docs
Expand Down Expand Up @@ -2039,9 +2066,9 @@ export interface AstroAssetsFeature {

export interface AstroInternationalizationFeature {
/**
* Whether the adapter is able to detect the language of the browser, usually using the `Accept-Language` header.
* The adapter should be able to create the proper redirects
*/
detectBrowserLanguage?: SupportsKind;
domain?: SupportsKind;
}

export interface AstroAdapter {
Expand Down
33 changes: 32 additions & 1 deletion packages/astro/src/core/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,16 @@ export const AstroConfigSchema = z.object({
defaultLocale: z.string(),
locales: z.string().array(),
fallback: z.record(z.string(), z.string()).optional(),
domains: z
.record(
z.string(),
z
.string()
.url(
"The domain value must be a valid URL, and it has to start with 'https' or 'http'."
)
)
.optional(),
// TODO: properly add default when the feature goes of experimental
routingStrategy: z
.enum(['prefix-always', 'prefix-other-locales'])
Expand All @@ -355,7 +365,7 @@ export const AstroConfigSchema = z.object({
.optional()
.superRefine((i18n, ctx) => {
if (i18n) {
const { defaultLocale, locales, fallback } = i18n;
const { defaultLocale, locales, fallback, domains } = i18n;
if (!locales.includes(defaultLocale)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
Expand Down Expand Up @@ -386,6 +396,27 @@ export const AstroConfigSchema = z.object({
}
}
}
if (domains) {
for (const [domainKey, domainValue] of Object.entries(domains)) {
if (!locales.includes(domainKey)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `The locale \`${domainKey}\` key in the \`i18n.domains\` record doesn't exist in the \`i18n.locales\` array.`,
});
}
try {
const domainUrl = new URL(domainValue);
if (domainUrl.pathname !== '/') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `The URL \`${domainValue}\` must contain only the origin. A subsequent pathname isn't allowed here. Remove \`${domainUrl.pathname}\`.`,
});
}
} catch {
// no need to catch the error
}
}
}
}
})
),
Expand Down
82 changes: 66 additions & 16 deletions packages/astro/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type GetLocaleRelativeUrl = GetLocaleOptions & {
format: AstroConfig['build']['format'];
routingStrategy?: 'prefix-always' | 'prefix-other-locales';
defaultLocale: string;
domains: Record<string, string>;
path: string;
};

export type GetLocaleOptions = {
Expand All @@ -20,10 +22,6 @@ export type GetLocaleOptions = {
* @default true
*/
normalizeLocale?: boolean;
/**
* An optional path to add after the `locale`.
*/
path?: string;
/**
* An optional path to prepend to `locale`.
*/
Expand All @@ -32,6 +30,7 @@ export type GetLocaleOptions = {

type GetLocaleAbsoluteUrl = GetLocaleRelativeUrl & {
site: AstroConfig['site'];
isBuild: boolean;
};
/**
* The base URL
Expand Down Expand Up @@ -73,17 +72,31 @@ export function getLocaleRelativeUrl({
/**
* The absolute URL
*/
export function getLocaleAbsoluteUrl({ site, ...rest }: GetLocaleAbsoluteUrl) {
const locale = getLocaleRelativeUrl(rest);
if (site) {
return joinPaths(site, locale);
export function getLocaleAbsoluteUrl({ site, isBuild, ...rest }: GetLocaleAbsoluteUrl) {
const localeUrl = getLocaleRelativeUrl(rest);
const { domains, locale } = rest;
let url;
if (isBuild) {
const base = domains[locale];
url = joinPaths(base, localeUrl.replace(`/${rest.locale}`, ''));
} else {
if (site) {
url = joinPaths(site, localeUrl);
} else {
url = localeUrl;
}
}

if (shouldAppendForwardSlash(rest.trailingSlash, rest.format)) {
return appendForwardSlash(url);
} else {
return locale;
return url;
}
}

type GetLocalesBaseUrl = GetLocaleOptions & {
base: string;
path: string;
locales: string[];
trailingSlash: AstroConfig['trailingSlash'];
format: AstroConfig['build']['format'];
Expand All @@ -98,7 +111,7 @@ export function getLocaleRelativeUrlList({
format,
path,
prependWith,
normalizeLocale = false,
normalizeLocale = true,
routingStrategy = 'prefix-other-locales',
defaultLocale,
}: GetLocalesBaseUrl) {
Expand All @@ -120,13 +133,50 @@ export function getLocaleRelativeUrlList({
});
}

export function getLocaleAbsoluteUrlList({ site, ...rest }: GetLocaleAbsoluteUrl) {
const locales = getLocaleRelativeUrlList(rest);
return locales.map((locale) => {
if (site) {
return joinPaths(site, locale);
export function getLocaleAbsoluteUrlList({
base,
locales,
trailingSlash,
format,
path,
prependWith,
normalizeLocale = true,
routingStrategy = 'prefix-other-locales',
defaultLocale,
isBuild,
domains,
site,
}: GetLocaleAbsoluteUrl) {
return locales.map((currentLocale) => {
const pathsToJoin = [];
const normalizedLocale = normalizeLocale ? normalizeTheLocale(currentLocale) : currentLocale;
const domainBase = domains ? domains[currentLocale] : undefined;
if (isBuild && domainBase) {
if (domainBase) {
pathsToJoin.push(domains[currentLocale]);
} else {
pathsToJoin.push(site);
}
pathsToJoin.push(base);
pathsToJoin.push(prependWith);
} else {
return locale;
if (site) {
pathsToJoin.push(site);
}
pathsToJoin.push(base);
pathsToJoin.push(prependWith);
if (routingStrategy === 'prefix-always') {
pathsToJoin.push(normalizedLocale);
} else if (currentLocale !== defaultLocale) {
pathsToJoin.push(normalizedLocale);
}
}

pathsToJoin.push(path);
if (shouldAppendForwardSlash(trailingSlash, format)) {
return appendForwardSlash(joinPaths(...pathsToJoin));
} else {
return joinPaths(...pathsToJoin);
}
});
}
Expand Down
15 changes: 13 additions & 2 deletions packages/astro/src/i18n/vite-plugin-i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type AstroInternationalization = {
export default function astroInternationalization({
settings,
}: AstroInternationalization): vite.Plugin {
let isCommandBuild = false;

return {
name: 'astro:i18n',
enforce: 'pre',
Expand All @@ -19,6 +21,11 @@ export default function astroInternationalization({
return resolvedVirtualModuleId;
}
},

config(opts, { command }) {
isCommandBuild = command === 'build';
return opts;
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `
Expand All @@ -35,13 +42,14 @@ export default function astroInternationalization({
const format = ${JSON.stringify(settings.config.build.format)};
const site = ${JSON.stringify(settings.config.site)};
const i18n = ${JSON.stringify(settings.config.experimental.i18n)};
const isBuild = ${isCommandBuild};
export const getRelativeLocaleUrl = (locale, path = "", opts) => _getLocaleRelativeUrl({
locale,
path,
base,
trailingSlash,
format,
format,
...i18n,
...opts
});
Expand All @@ -52,13 +60,16 @@ export default function astroInternationalization({
trailingSlash,
format,
site,
isBuild,
...i18n,
...opts
});
export const getRelativeLocaleUrlList = (path = "", opts) => _getLocaleRelativeUrlList({
base, path, trailingSlash, format, ...i18n, ...opts });
export const getAbsoluteLocaleUrlList = (path = "", opts) => _getLocaleAbsoluteUrlList({ base, path, trailingSlash, format, site, ...i18n, ...opts });
export const getAbsoluteLocaleUrlList = (path = "", opts) => _getLocaleAbsoluteUrlList({
base, path, trailingSlash, format, site, isBuild, ...i18n, ...opts
});
`;
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/integrations/astroFeaturesValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const ALL_UNSUPPORTED: Required<AstroFeatureMap> = {
hybridOutput: UNSUPPORTED,
assets: UNSUPPORTED_ASSETS_FEATURE,
i18n: {
detectBrowserLanguage: UNSUPPORTED,
domain: UNSUPPORTED,
},
};

Expand Down
5 changes: 4 additions & 1 deletion packages/astro/test/fixtures/i18n-routing/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ export default defineConfig({
defaultLocale: 'en',
locales: [
'en', 'pt', 'it'
]
],
domains: {
it: "https://it.example.com"
}
}
},
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
---
import { getRelativeLocaleUrl } from "astro:i18n";
import { getRelativeLocaleUrl, getAbsoluteLocaleUrl } from "astro:i18n";
let about = getRelativeLocaleUrl("pt", "about");
let italianAbout = getAbsoluteLocaleUrl("it", "about");
---

Expand All @@ -13,5 +14,6 @@ let about = getRelativeLocaleUrl("pt", "about");
Virtual module doesn't break

About: {about}
About it: {italianAbout}
</body>
</html>
18 changes: 18 additions & 0 deletions packages/astro/test/i18n-routing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ describe('astro:i18n virtual module', () => {
const text = await response.text();
expect(text).includes("Virtual module doesn't break");
expect(text).includes('About: /pt/about');
expect(text).includes('About it: /it/about');
});

describe('absolute URLs', () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/i18n-routing/',
});
await fixture.build();
});

it('correctly renders the absolute URL', async () => {
const html = await fixture.readFile('/virtual-module/index.html');
let $ = cheerio.load(html);

expect($('body').text()).includes("Virtual module doesn't break");
expect($('body').text()).includes('About it: https://it.example.com/about');
});
});
});
describe('[DEV] i18n routing', () => {
Expand Down
Loading

0 comments on commit 07d2f95

Please sign in to comment.