Skip to content

feat: internationalization #38

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
21 changes: 8 additions & 13 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import type { StorybookConfig } from "@storybook/nextjs-vite";

const config: StorybookConfig = {
"stories": [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
],
"addons": [
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@chromatic-com/storybook",
"@storybook/addon-docs",
"@storybook/addon-a11y"
"@storybook/addon-a11y",
],
"framework": {
"name": "@storybook/nextjs-vite",
"options": {}
framework: {
name: "@storybook/nextjs-vite",
options: {},
},
"staticDirs": [
"../public"
]
staticDirs: ["../public"],
};
export default config;
export default config;
1 change: 0 additions & 1 deletion .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import "../src/app/globals.css";
import type { Preview } from "@storybook/nextjs-vite";

Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const compat = new FlatCompat({

const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
...storybook.configs["flat/recommended"]
...storybook.configs["flat/recommended"],
];

export default eslintConfig;
2 changes: 1 addition & 1 deletion postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const config = {
plugins: {"@tailwindcss/postcss": {}},
plugins: { "@tailwindcss/postcss": {} },
};

export default config;
14 changes: 7 additions & 7 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,17 @@
}

@theme {
--color-primary: #EE7749;
--color-light: #FFFFFF;
--color-primary: #ee7749;
--color-light: #ffffff;
--color-dark: #000000;
--color-muted: #FAFAFA;
--color-muted: #fafafa;
--color-university: #971318;
--color-danger: #FF5E79;
--color-success: #2EDB51;
--color-danger: #ff5e79;
--color-success: #2edb51;

--font-jamjuree: var(--font-jamjuree)
--font-jamjuree: var(--font-jamjuree);
}

input[type="number"] {
-moz-appearance: textfield;
}
}
11 changes: 5 additions & 6 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { Metadata } from "next";
import { Bai_Jamjuree } from "next/font/google";
import "./globals.css";
import { DictionaryProvider } from "@/contexts/dictionary-provider";

const jamjuree = Bai_Jamjuree({
subsets: ["latin"],
variable: "--font-jamjuree",
weight: ["200", "300", "400", "500", "600", "700"],
display: "swap"
})
display: "swap",
});

export const metadata: Metadata = {
title: "Create Next App",
Expand All @@ -21,10 +22,8 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body
className={`${jamjuree.variable} font-jamjuree antialiased`}
>
{children}
<body className={`${jamjuree.variable} font-jamjuree antialiased`}>
<DictionaryProvider>{children}</DictionaryProvider>
</body>
</html>
);
Expand Down
6 changes: 4 additions & 2 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ interface ICardProps {

function Card({ children, className }: ICardProps) {
return (
<div className={`${className} p-4 border rounded-[15px] border-[#eeeeee] bg-[#fafafa]`}>
<div
className={`${className} rounded-[15px] border border-[#eeeeee] bg-[#fafafa] p-4`}
>
{children}
</div>
);
}

export default Card;
export default Card;
31 changes: 21 additions & 10 deletions src/components/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,33 @@ interface IInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
center_text?: boolean;
}

export default function Input({ placeholder, type, disabled, className, name, value, center_text, min, max, onChange, ...rest }: IInputProps) {

const textAlignment = center_text ? 'text-center' : 'text-left';
export default function Input({
placeholder,
type,
disabled,
className,
name,
value,
center_text,
min,
max,
onChange,
...rest
}: IInputProps) {
const textAlignment = center_text ? "text-center" : "text-left";

return (
<input
<input
type={type}
placeholder={placeholder}
disabled={disabled}
name={name}
name={name}
value={value}
onChange={onChange}
className={`${className} ${textAlignment} rounded-xl border border-black/10 outline-none text-black px-3 py-2.5 text-lg placeholder:text-black/30 invalid:border-red-500 invalid:text-red-600`}
min={type === 'number' ? min : undefined}
max={type === 'number' ? max : undefined}
className={`${className} ${textAlignment} rounded-xl border border-black/10 px-3 py-2.5 text-lg text-black outline-none placeholder:text-black/30 invalid:border-red-500 invalid:text-red-600`}
min={type === "number" ? min : undefined}
max={type === "number" ? max : undefined}
{...rest}
/>
)
}
);
}
30 changes: 15 additions & 15 deletions src/components/label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,36 @@ interface ILabelProps {
children: ReactNode;
disabled?: boolean;
htmlFor?: string;
size?: 'small' | 'medium' | 'large';
size?: "small" | "medium" | "large";
onClick?: () => void;
}

const Label = ({
children,
disabled = false,
const Label = ({
children,
disabled = false,
htmlFor,
size = 'medium',
size = "medium",
onClick,
}: ILabelProps) => {
const getSizeClasses = () => {
switch (size) {
case 'small':
return 'text-xs';
case 'large':
return 'text-base';
case "small":
return "text-xs";
case "large":
return "text-base";
default:
return 'text-sm';
return "text-sm";
}
};

const colorClass = disabled ? 'text-gray-400' : 'text-gray-700';
const cursorClass = disabled ? 'cursor-not-allowed' : 'cursor-pointer';
const colorClass = disabled ? "text-gray-400" : "text-gray-700";
const cursorClass = disabled ? "cursor-not-allowed" : "cursor-pointer";

return (
<label
htmlFor={htmlFor}
onClick={disabled ? undefined : onClick}
className={`${getSizeClasses()} ${colorClass} ${cursorClass}`}
htmlFor={htmlFor}
onClick={disabled ? undefined : onClick}
className={`${getSizeClasses()} ${colorClass} ${cursorClass}`}
>
{children}
</label>
Expand Down
82 changes: 82 additions & 0 deletions src/contexts/dictionary-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"use client";

import { createContext, useContext, useEffect, useState } from "react";
import type { Dictionary, Language } from "@/internationalization/dictionaries";
import { getDictionary } from "@/internationalization/dictionaries";

export type DictionaryLanguage = Language;

interface DictionaryContextData {
dictionary: Dictionary;
language: DictionaryLanguage;
}

const DictionaryContext = createContext<DictionaryContextData | undefined>(
undefined,
);

/* FIXME: Check this function when the api is available */
export async function PreferedLanguage(): Promise<DictionaryLanguage> {
const data = await fetch("https://api/preferences/language");
if (!data.ok) {
throw new Error("Failed to fetch preferred language");
}
const { language } = await data.json();
return language as DictionaryLanguage;
}

export function getBrowserLanguage(): DictionaryLanguage {
if (typeof navigator !== "undefined" && navigator.language) {
return navigator.language as DictionaryLanguage;
}
return "en-US";
}

export function DictionaryProvider({
children,
language: propLanguage,
}: {
children: React.ReactNode;
language?: DictionaryLanguage;
}) {
const [language, setLanguage] = useState<DictionaryLanguage>(
propLanguage || "en-US",
);

useEffect(() => {
if (!propLanguage) {
(async () => {
try {
const preferredLanguage = await PreferedLanguage();
setLanguage(preferredLanguage);
} catch {
setLanguage(getBrowserLanguage());
}
})();
}
}, [propLanguage]);

const dictionary = getDictionary(language);

return (
<DictionaryContext.Provider value={{ dictionary, language }}>
{children}
</DictionaryContext.Provider>
);
}

export function useDictionary() {
const context = useContext(DictionaryContext);
if (!context) {
throw new Error("useDictionary must be used within a DictionaryProvider");
}
return context.dictionary;
}

export function useLanguage() {
const context = useContext(DictionaryContext);
if (!context) {
throw new Error("useLanguage must be used within a DictionaryProvider");
}
return context.language;
}
14 changes: 14 additions & 0 deletions src/internationalization/dictionaries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import en from "./dictionaries/en.json";
import pt from "./dictionaries/pt.json";

const dictionaries = {
"en-US": en,
"pt-PT": pt,
};

export type Language = keyof typeof dictionaries;
export type Dictionary = (typeof dictionaries)[Language];

export const getDictionary = (lang: Language): Dictionary => {
return dictionaries[lang] || dictionaries["en-US"];
};
Empty file.
Empty file.
26 changes: 13 additions & 13 deletions src/stories/Button.stories.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import type { Meta, StoryObj } from "@storybook/nextjs-vite";

import { fn } from 'storybook/test';
import { fn } from "storybook/test";

import { Button } from './Button';
import { Button } from "./Button";

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: 'Example/Button',
title: "Example/Button",
component: Button,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
layout: "centered",
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
tags: ["autodocs"],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
backgroundColor: { control: "color" },
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onClick: fn() },
Expand All @@ -29,26 +29,26 @@ type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
primary: true,
label: 'Button',
label: "Button",
},
};

export const Secondary: Story = {
args: {
label: 'Button',
label: "Button",
},
};

export const Large: Story = {
args: {
size: 'large',
label: 'Button',
size: "large",
label: "Button",
},
};

export const Small: Story = {
args: {
size: 'small',
label: 'Button',
size: "small",
label: "Button",
},
};
Loading