mirror of
https://github.com/withastro/astro.git
synced 2025-04-07 23:41:43 -05:00
feat(i18n): add Astro.currentLocale
(#9101)
This commit is contained in:
parent
8366cd7775
commit
e3dce215a5
17 changed files with 222 additions and 16 deletions
5
.changeset/quick-toes-peel.md
Normal file
5
.changeset/quick-toes-peel.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Add a new property `Astro.currentLocale`, available when `i18n` is enabled.
|
|
@ -2112,6 +2112,11 @@ interface AstroSharedContext<
|
|||
*/
|
||||
|
||||
preferredLocaleList: string[] | undefined;
|
||||
|
||||
/**
|
||||
* The current locale computed from the URL of the request. It matches the locales in `i18n.locales`, and returns `undefined` otherwise.
|
||||
*/
|
||||
currentLocale: string | undefined;
|
||||
}
|
||||
|
||||
export interface APIContext<
|
||||
|
@ -2241,6 +2246,11 @@ export interface APIContext<
|
|||
* [quality value]: https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
|
||||
*/
|
||||
preferredLocaleList: string[] | undefined;
|
||||
|
||||
/**
|
||||
* The current locale computed from the URL of the request. It matches the locales in `i18n.locales`, and returns `undefined` otherwise.
|
||||
*/
|
||||
currentLocale: string | undefined;
|
||||
}
|
||||
|
||||
export type EndpointOutput =
|
||||
|
|
|
@ -234,7 +234,9 @@ export class App {
|
|||
status,
|
||||
env: this.#pipeline.env,
|
||||
mod: handler as any,
|
||||
locales: this.#manifest.i18n ? this.#manifest.i18n.locales : undefined,
|
||||
locales: this.#manifest.i18n?.locales,
|
||||
routingStrategy: this.#manifest.i18n?.routingStrategy,
|
||||
defaultLocale: this.#manifest.i18n?.defaultLocale,
|
||||
});
|
||||
} else {
|
||||
const pathname = prependForwardSlash(this.removeBase(url.pathname));
|
||||
|
@ -269,7 +271,9 @@ export class App {
|
|||
status,
|
||||
mod,
|
||||
env: this.#pipeline.env,
|
||||
locales: this.#manifest.i18n ? this.#manifest.i18n.locales : undefined,
|
||||
locales: this.#manifest.i18n?.locales,
|
||||
routingStrategy: this.#manifest.i18n?.routingStrategy,
|
||||
defaultLocale: this.#manifest.i18n?.defaultLocale,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -558,7 +558,9 @@ async function generatePath(pathname: string, gopts: GeneratePathOptions, pipeli
|
|||
route: pageData.route,
|
||||
env: pipeline.getEnvironment(),
|
||||
mod,
|
||||
locales: i18n ? i18n.locales : undefined,
|
||||
locales: i18n?.locales,
|
||||
routingStrategy: i18n?.routingStrategy,
|
||||
defaultLocale: i18n?.defaultLocale,
|
||||
});
|
||||
|
||||
let body: string | Uint8Array;
|
||||
|
|
|
@ -12,7 +12,11 @@ import { ASTRO_VERSION } from '../constants.js';
|
|||
import { AstroCookies, attachCookiesToResponse } from '../cookies/index.js';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||
import { computePreferredLocale, computePreferredLocaleList } from '../render/context.js';
|
||||
import {
|
||||
computeCurrentLocale,
|
||||
computePreferredLocale,
|
||||
computePreferredLocaleList,
|
||||
} from '../render/context.js';
|
||||
import { type Environment, type RenderContext } from '../render/index.js';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
@ -27,6 +31,8 @@ type CreateAPIContext = {
|
|||
props: Record<string, any>;
|
||||
adapterName?: string;
|
||||
locales: string[] | undefined;
|
||||
routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined;
|
||||
defaultLocale: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -41,9 +47,12 @@ export function createAPIContext({
|
|||
props,
|
||||
adapterName,
|
||||
locales,
|
||||
routingStrategy,
|
||||
defaultLocale,
|
||||
}: CreateAPIContext): APIContext {
|
||||
let preferredLocale: string | undefined = undefined;
|
||||
let preferredLocaleList: string[] | undefined = undefined;
|
||||
let currentLocale: string | undefined = undefined;
|
||||
|
||||
const context = {
|
||||
cookies: new AstroCookies(request),
|
||||
|
@ -83,6 +92,16 @@ export function createAPIContext({
|
|||
|
||||
return undefined;
|
||||
},
|
||||
get currentLocale(): string | undefined {
|
||||
if (currentLocale) {
|
||||
return currentLocale;
|
||||
}
|
||||
if (locales) {
|
||||
currentLocale = computeCurrentLocale(request, locales, routingStrategy, defaultLocale);
|
||||
}
|
||||
|
||||
return currentLocale;
|
||||
},
|
||||
url: new URL(request.url),
|
||||
get clientAddress() {
|
||||
if (clientAddressSymbol in request) {
|
||||
|
@ -153,8 +172,7 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
|
|||
mod: EndpointHandler,
|
||||
env: Environment,
|
||||
ctx: RenderContext,
|
||||
onRequest: MiddlewareHandler<MiddlewareResult> | undefined,
|
||||
locales: undefined | string[]
|
||||
onRequest: MiddlewareHandler<MiddlewareResult> | undefined
|
||||
): Promise<Response> {
|
||||
const context = createAPIContext({
|
||||
request: ctx.request,
|
||||
|
@ -162,7 +180,9 @@ export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>
|
|||
props: ctx.props,
|
||||
site: env.site,
|
||||
adapterName: env.adapterName,
|
||||
locales,
|
||||
routingStrategy: ctx.routingStrategy,
|
||||
defaultLocale: ctx.defaultLocale,
|
||||
locales: ctx.locales,
|
||||
});
|
||||
|
||||
let response;
|
||||
|
|
|
@ -35,6 +35,8 @@ function createContext({ request, params, userDefinedLocales = [] }: CreateConte
|
|||
props: {},
|
||||
site: undefined,
|
||||
locales: userDefinedLocales,
|
||||
defaultLocale: undefined,
|
||||
routingStrategy: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -128,6 +128,8 @@ export class Pipeline {
|
|||
site: env.site,
|
||||
adapterName: env.adapterName,
|
||||
locales: renderContext.locales,
|
||||
routingStrategy: renderContext.routingStrategy,
|
||||
defaultLocale: renderContext.defaultLocale,
|
||||
});
|
||||
|
||||
switch (renderContext.route.type) {
|
||||
|
@ -158,13 +160,7 @@ export class Pipeline {
|
|||
}
|
||||
}
|
||||
case 'endpoint': {
|
||||
return await callEndpoint(
|
||||
mod as any as EndpointHandler,
|
||||
env,
|
||||
renderContext,
|
||||
onRequest,
|
||||
renderContext.locales
|
||||
);
|
||||
return await callEndpoint(mod as any as EndpointHandler, env, renderContext, onRequest);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Couldn't find route of type [${renderContext.route.type}]`);
|
||||
|
|
|
@ -29,6 +29,8 @@ export interface RenderContext {
|
|||
props: Props;
|
||||
locals?: object;
|
||||
locales: string[] | undefined;
|
||||
defaultLocale: string | undefined;
|
||||
routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined;
|
||||
}
|
||||
|
||||
export type CreateRenderContextArgs = Partial<
|
||||
|
@ -60,6 +62,8 @@ export async function createRenderContext(
|
|||
params,
|
||||
props,
|
||||
locales: options.locales,
|
||||
routingStrategy: options.routingStrategy,
|
||||
defaultLocale: options.defaultLocale,
|
||||
};
|
||||
|
||||
// We define a custom property, so we can check the value passed to locals
|
||||
|
@ -208,3 +212,23 @@ export function computePreferredLocaleList(request: Request, locales: string[])
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function computeCurrentLocale(
|
||||
request: Request,
|
||||
locales: string[],
|
||||
routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined,
|
||||
defaultLocale: string | undefined
|
||||
): undefined | string {
|
||||
const requestUrl = new URL(request.url);
|
||||
for (const segment of requestUrl.pathname.split('/')) {
|
||||
for (const locale of locales) {
|
||||
if (normalizeTheLocale(locale) === normalizeTheLocale(segment)) {
|
||||
return locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (routingStrategy === 'prefix-other-locales') {
|
||||
return defaultLocale;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -60,6 +60,8 @@ export async function renderPage({ mod, renderContext, env, cookies }: RenderPag
|
|||
cookies,
|
||||
locals: renderContext.locals ?? {},
|
||||
locales: renderContext.locales,
|
||||
defaultLocale: renderContext.defaultLocale,
|
||||
routingStrategy: renderContext.routingStrategy,
|
||||
});
|
||||
|
||||
// TODO: Remove in Astro 4.0
|
||||
|
|
|
@ -12,7 +12,11 @@ import { chunkToString } from '../../runtime/server/render/index.js';
|
|||
import { AstroCookies } from '../cookies/index.js';
|
||||
import { AstroError, AstroErrorData } from '../errors/index.js';
|
||||
import type { Logger } from '../logger/core.js';
|
||||
import { computePreferredLocale, computePreferredLocaleList } from './context.js';
|
||||
import {
|
||||
computeCurrentLocale,
|
||||
computePreferredLocale,
|
||||
computePreferredLocaleList,
|
||||
} from './context.js';
|
||||
|
||||
const clientAddressSymbol = Symbol.for('astro.clientAddress');
|
||||
const responseSentSymbol = Symbol.for('astro.responseSent');
|
||||
|
@ -47,6 +51,8 @@ export interface CreateResultArgs {
|
|||
locals: App.Locals;
|
||||
cookies?: AstroCookies;
|
||||
locales: string[] | undefined;
|
||||
defaultLocale: string | undefined;
|
||||
routingStrategy: 'prefix-always' | 'prefix-other-locales' | undefined;
|
||||
}
|
||||
|
||||
function getFunctionExpression(slot: any) {
|
||||
|
@ -148,6 +154,7 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
|||
let cookies: AstroCookies | undefined = args.cookies;
|
||||
let preferredLocale: string | undefined = undefined;
|
||||
let preferredLocaleList: string[] | undefined = undefined;
|
||||
let currentLocale: string | undefined = undefined;
|
||||
|
||||
// Create the result object that will be passed into the render function.
|
||||
// This object starts here as an empty shell (not yet the result) but then
|
||||
|
@ -218,6 +225,24 @@ export function createResult(args: CreateResultArgs): SSRResult {
|
|||
|
||||
return undefined;
|
||||
},
|
||||
get currentLocale(): string | undefined {
|
||||
if (currentLocale) {
|
||||
return currentLocale;
|
||||
}
|
||||
if (args.locales) {
|
||||
currentLocale = computeCurrentLocale(
|
||||
request,
|
||||
args.locales,
|
||||
args.routingStrategy,
|
||||
args.defaultLocale
|
||||
);
|
||||
if (currentLocale) {
|
||||
return currentLocale;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
params,
|
||||
props,
|
||||
locals,
|
||||
|
|
|
@ -215,6 +215,9 @@ export async function handleRoute({
|
|||
env,
|
||||
mod,
|
||||
route,
|
||||
locales: manifest.i18n?.locales,
|
||||
routingStrategy: manifest.i18n?.routingStrategy,
|
||||
defaultLocale: manifest.i18n?.defaultLocale,
|
||||
});
|
||||
} else {
|
||||
return handle404Response(origin, incomingRequest, incomingResponse);
|
||||
|
@ -271,7 +274,9 @@ export async function handleRoute({
|
|||
route: options.route,
|
||||
mod,
|
||||
env,
|
||||
locales: i18n ? i18n.locales : undefined,
|
||||
locales: i18n?.locales,
|
||||
routingStrategy: i18n?.routingStrategy,
|
||||
defaultLocale: i18n?.defaultLocale,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
---
|
||||
const currentLocale = Astro.currentLocale;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Start
|
||||
Current Locale: {currentLocale ? currentLocale : "none"}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
---
|
||||
const currentLocale = Astro.currentLocale;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Oi essa e start
|
||||
Current Locale: {currentLocale ? currentLocale : "none"}
|
||||
</body>
|
||||
</html>
|
||||
|
|
12
packages/astro/test/fixtures/i18n-routing/src/pages/current-locale.astro
vendored
Normal file
12
packages/astro/test/fixtures/i18n-routing/src/pages/current-locale.astro
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
const currentLocale = Astro.currentLocale;
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Current Locale: {currentLocale ? currentLocale : "none"}
|
||||
</body>
|
||||
</html>
|
19
packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro
vendored
Normal file
19
packages/astro/test/fixtures/i18n-routing/src/pages/dynamic/[id].astro
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
|
||||
---
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{ id: "lorem" }
|
||||
]
|
||||
}
|
||||
const currentLocale = Astro.currentLocale;
|
||||
|
||||
---
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Current Locale: {currentLocale ? currentLocale : "none"}
|
||||
</body>
|
||||
</html>
|
|
@ -1,8 +1,12 @@
|
|||
---
|
||||
const currentLocale = Astro.currentLocale;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
Hola
|
||||
Current Locale: {currentLocale ? currentLocale : "none"}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -991,6 +991,73 @@ describe('[SSR] i18n routing', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('current locale', () => {
|
||||
describe('with [prefix-other-locales]', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('should return the default locale', async () => {
|
||||
let request = new Request('http://example.com/current-locale', {});
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Current Locale: en');
|
||||
});
|
||||
|
||||
it('should return the default locale of the current URL', async () => {
|
||||
let request = new Request('http://example.com/pt/start', {});
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Current Locale: pt');
|
||||
});
|
||||
|
||||
it('should return the default locale when a route is dynamic', async () => {
|
||||
let request = new Request('http://example.com/dynamic/lorem', {});
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Current Locale: en');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with [prefix-always]', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/i18n-routing-prefix-always/',
|
||||
output: 'server',
|
||||
adapter: testAdapter(),
|
||||
});
|
||||
await fixture.build();
|
||||
app = await fixture.loadTestAdapterApp();
|
||||
});
|
||||
|
||||
it('should return the locale of the current URL (en)', async () => {
|
||||
let request = new Request('http://example.com/en/start', {});
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Current Locale: en');
|
||||
});
|
||||
|
||||
it('should return the locale of the current URL (pt)', async () => {
|
||||
let request = new Request('http://example.com/pt/start', {});
|
||||
let response = await app.render(request);
|
||||
expect(response.status).to.equal(200);
|
||||
expect(await response.text()).includes('Current Locale: pt');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n routing does not break assets and endpoints', () => {
|
Loading…
Add table
Reference in a new issue