Skip to content
Draft

I18n #372

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions examples/next/app/[locale]/blog/[blog]/en-us.val.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { c } from "../../../../val.config";
import { schema } from "./schema.val";

export default c.define("/app/[locale]/blogs/[blog]/en-us.val.ts", schema, {
"/en-us/blogs/my-page": {
title: "My page",
author: "freekh",
content: [
{
tag: "p",
children: ["English content"],
},
],
link: {
href: "/",
label: "Home",
},
translation: "/nb-no/blogs/min-side",
},
});
20 changes: 20 additions & 0 deletions examples/next/app/[locale]/blog/[blog]/nb-no.val.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { c } from "../../../../val.config";
import { schema } from "./schema.val";

export default c.define("/app/[locale]/blogs/[blog]/nb-no.val.ts", schema, {
"/nb-no/blogs/min-side": {
title: "Min side",
author: "thoram",
content: [
{
tag: "p",
children: ["Norsk innhold"],
},
],
link: {
href: "/",
label: "Hjem",
},
translation: "/en-us/blogs/my-page",
},
});
45 changes: 45 additions & 0 deletions examples/next/app/[locale]/blog/[blog]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use server";
import { notFound } from "next/navigation";
import { fetchVal, fetchValRoute } from "../../../../val/rsc";
import Link from "next/link";
import authorsVal from "../../../../content/authors.val";
import { ValRichText } from "@valbuild/next";
import enVal from "./en-us.val";
import nbVal from "./nb-no.val";
import { Blog } from "./schema.val";
import translationsVal from "./translations.val";

export default async function BlogPage({
params,
}: {
params: Promise<{ blog: string; locale: string }>;
}) {
const { locale } = await params;
const translations = await fetchVal(translationsVal);
const blog = await fetchValRoute([enVal, nbVal], params);

const authors = await fetchVal(authorsVal);
if (!blog) {
return notFound();
}
const author = authors[blog.author];
return (
<div>
<h1>{blog.title}</h1>
<aside>Author: {author.name}</aside>
<ValRichText>{blog.content}</ValRichText>
<Link href={blog.link.href}>{blog.link.label}</Link>
{blog.translations?.length > 0 && (
<p>
{translations[locale]?.translationCanBeFound}
{blog.translations.map((translation) => (
<Link key={translation} href={translation}>
{translation}
</Link>
))}
</p>
)}
s
</div>
);
}
28 changes: 28 additions & 0 deletions examples/next/app/[locale]/blog/[blog]/schema.val.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import authorsVal from "../../../../content/authors.val";
import { nextAppRouter, s, t } from "../../../../val.config";

const blogSchema = s.object({
title: s.string(),
translations: s.array(
s.object({
locale: s.string(),
key: s.string(),
}),
),
author: s.keyOf(authorsVal),
content: s.richtext(),
link: s.object({
href: s.string(),
label: s.string(),
}),
});

export type Blog = t.inferSchema<typeof blogSchema>;

export const schema = s.record(blogSchema).router(
nextAppRouter.localize({
moduleName: "locale",
segment: "locale",
translations: "translations",
}),
Comment on lines +23 to +27
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@isakgb this means that this router is not in page.val.ts, but the module names are the locale. In addtion: the locale segment is "locale" (and must be an actual locale).

);
28 changes: 28 additions & 0 deletions examples/next/app/[locale]/blog/[blog]/translations.val.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { c, s } from "../../../../val.config";

export default c.define(
"/app/[locale]/blog/[blog]/translations.val.ts",
s
.record(
s.object({
"en-us": s.string(),
"nb-no": s.string(),
"translation-is-available-in": s.string(),
}),
)
.keys({ locale: { required: ["en-us", "nb-no"] } }),
{
"en-us": {
"en-us": "English",
"nb-no": "Norwegian",
"translation-is-available-in":
"This blog is also available in the following languages:",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@isakgb This is useful if you need translations that are "by field".

},
"nb-no": {
"en-us": "Engelsk",
"nb-no": "Norsk",
"translation-is-available-in":
"Denne bloggen er også tilgjengelig på følgende språk:",
},
},
);
28 changes: 0 additions & 28 deletions examples/next/app/blogs/[blog]/page.tsx

This file was deleted.

49 changes: 0 additions & 49 deletions examples/next/app/blogs/[blog]/page.val.ts

This file was deleted.

2 changes: 1 addition & 1 deletion examples/next/components/link.val.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { s } from "../val.config";
import blogsVal from "../app/blogs/[blog]/page.val";
import blogsVal from "../app/[locale]/blog/[blog]/page.val";
import genericPageVal from "../app/generic/[[...path]]/page.val";
import { Schema } from "@valbuild/core";
import pageVal from "../app/page.val";
Expand Down
1 change: 0 additions & 1 deletion examples/next/val.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { initVal } from "@valbuild/next";
const { s, c, val, config, nextAppRouter } = initVal({
project: "valbuild/val-examples-next",
root: "/examples/next",
defaultTheme: "dark",
});

export type { t } from "@valbuild/next";
Expand Down
3 changes: 2 additions & 1 deletion examples/next/val.modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { config } from "./val.config";

export default modules(config, [
{ def: () => import("./content/authors.val") },
{ def: () => import("./app/blogs/[blog]/page.val") },
{ def: () => import("./app/[locale]/blog/[blog]/en-us.val") },
{ def: () => import("./app/[locale]/blog/[blog]/nb-no.val") },
{ def: () => import("./app/generic/[[...path]]/page.val") },
{ def: () => import("./app/page.val") },
]);
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export {
type ListArrayRender,
type ReifiedRender,
} from "./render";
export type { ValRouter, RouteValidationError } from "./router";
export type { ValRouter, NextAppRouter, RouteValidationError } from "./router";
import { nextAppRouter } from "./router";

export const FATAL_ERROR_TYPES = [
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function validateLocale(locale: string): false | string {
if (locale.match(/^[a-z]{2}-[a-z]{2}$/)) {
return false;
}
return "Invalid locale format. Must be two lower case letters for language and two lowercase letters for country, separated by a hyphen. Expected format: xx-xx (e.g. en-us, nb-no)";
}
25 changes: 24 additions & 1 deletion packages/core/src/router.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { parseNextJsRoutePattern } from "./router";
import { NextAppRouterImpl, parseNextJsRoutePattern } from "./router";
import { object } from "./schema/object";
import { record } from "./schema/record";
import { string } from "./schema/string";
import { ModuleFilePath } from "./val";

describe("parseNextJsRoutePattern", () => {
describe("App Router patterns", () => {
Expand Down Expand Up @@ -155,4 +159,23 @@ describe("parseNextJsRoutePattern", () => {
).toEqual(["docs", "[...slug]"]);
});
});

describe("Localization", () => {
const router = new NextAppRouterImpl(
record(object({ title: string() })),
).localize({
type: "directory",
segment: "locale",
translation: "translation",
});

test("validate", () => {
expect(
router.validate(
"/app/[locale]/blogs/[blog]/page.val.ts" as ModuleFilePath,
["/en/blogs/test"],
),
).toEqual([]);
});
});
});
Loading
Loading