Skip to content

Commit 0800a1d

Browse files
committed
translations
1 parent 90d006c commit 0800a1d

19 files changed

Lines changed: 991 additions & 126 deletions

app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export default async function RootLayout({
102102
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
103103
/>
104104
</head>
105-
<body>
105+
<body suppressHydrationWarning>
106106
<ProgressProvider>
107107
<I18nProvider initialMessages={messages}>{children}</I18nProvider>
108108
</ProgressProvider>

app/welcome/page.tsx

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
1-
"use client";
1+
import { Metadata } from "next";
2+
import { getTranslations } from "next-intl/server";
3+
import { WelcomeView } from "@/components/landing/welcome-view";
24

3-
import { LandingHeader } from "@/components/landing/header";
4-
import { LandingFooter } from "@/components/landing/footer";
5-
import { LandingHero } from "@/components/landing/hero";
6-
import { LandingFeatures } from "@/components/landing/features";
7-
import { LandingEditors } from "@/components/landing/editors";
8-
import { LandingComparison } from "@/components/landing/comparison";
9-
import { LandingHeroCTA } from "@/components/landing/hero-cta";
10-
import { LandingAboutInled } from "@/components/landing/about-inled";
5+
export async function generateMetadata(): Promise<Metadata> {
6+
// We use the default locale for server-side metadata generation
7+
// Since the actual language is determined on the client via store/cookies
8+
const t = await getTranslations("landing.seo");
9+
10+
return {
11+
title: t("title"),
12+
description: t("description"),
13+
keywords: t("keywords"),
14+
alternates: {
15+
canonical: "https://office.inled.es/welcome",
16+
languages: {
17+
"es-ES": "https://office.inled.es/welcome?lang=es",
18+
"en-US": "https://office.inled.es/welcome?lang=en",
19+
"de-DE": "https://office.inled.es/welcome?lang=de",
20+
"fr-FR": "https://office.inled.es/welcome?lang=fr",
21+
"zh-CN": "https://office.inled.es/welcome?lang=zh-CN",
22+
},
23+
},
24+
openGraph: {
25+
title: t("title"),
26+
description: t("description"),
27+
images: ["https://hosted.inled.es/insuite-office-matrix.gif"],
28+
},
29+
twitter: {
30+
card: "summary_large_image",
31+
title: t("title"),
32+
description: t("description"),
33+
images: ["https://hosted.inled.es/insuite-office-matrix.gif"],
34+
},
35+
};
36+
}
1137

1238
export default function WelcomePage() {
13-
return (
14-
<div className="min-h-screen bg-white text-foreground selection:bg-primary/20">
15-
<LandingHeader />
16-
<main>
17-
<LandingHero />
18-
<LandingFeatures />
19-
<LandingEditors />
20-
<LandingComparison />
21-
<LandingHeroCTA />
22-
<LandingAboutInled />
23-
</main>
24-
<LandingFooter />
25-
</div>
26-
);
39+
return <WelcomeView />;
2740
}

components/i18n-provider.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const messagesCache: Partial<Record<Locale, AbstractIntlMessages>> = {};
1111

1212
// Load messages for a locale (with fallback to English)
1313
async function loadMessages(locale: Locale): Promise<AbstractIntlMessages> {
14+
// Normalize locale for file loading (e.g. es-419 -> es) if specific file doesn't exist
15+
// But first try exact match
1416
if (messagesCache[locale]) {
1517
return messagesCache[locale]!;
1618
}
@@ -20,6 +22,16 @@ async function loadMessages(locale: Locale): Promise<AbstractIntlMessages> {
2022
messagesCache[locale] = messages;
2123
return messages;
2224
} catch {
25+
// If es-419 fails, try es
26+
if (locale.includes("-")) {
27+
const baseLocale = locale.split("-")[0] as Locale;
28+
try {
29+
const messages = (await import(`@/messages/${baseLocale}.json`)).default;
30+
messagesCache[locale] = messages;
31+
return messages;
32+
} catch (e) {}
33+
}
34+
2335
// Fallback to English if locale file doesn't exist
2436
if (locale !== Locale.EN) {
2537
console.warn(
@@ -37,15 +49,11 @@ interface I18nProviderProps {
3749
initialMessages: AbstractIntlMessages;
3850
}
3951

40-
/**
41-
* Client-side i18n provider that switches language based on store setting.
42-
* Dynamically loads message files when language changes.
43-
*/
4452
export function I18nProvider({ children, initialMessages }: I18nProviderProps) {
4553
const locale = useResolvedLanguage();
4654
const [messages, setMessages] =
4755
useState<AbstractIntlMessages>(initialMessages);
48-
const [currentLocale, setCurrentLocale] = useState<Locale>(Locale.EN);
56+
const [currentLocale, setCurrentLocale] = useState<Locale>(locale);
4957

5058
useEffect(() => {
5159
// Load messages when locale changes
@@ -60,6 +68,11 @@ export function I18nProvider({ children, initialMessages }: I18nProviderProps) {
6068
locale={currentLocale}
6169
messages={messages}
6270
timeZone={getTimeZone(currentLocale)}
71+
onError={() => {}} // Silence missing message errors in production/dev if desired
72+
getMessageFallback={({ key, namespace }) => {
73+
// Fallback to the key itself if message is missing
74+
return namespace ? `${namespace}.${key}` : key;
75+
}}
6376
>
6477
{children}
6578
</NextIntlClientProvider>

components/landing/about-inled.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
"use client";
22

3+
import { useTranslations } from "next-intl";
4+
35
export function LandingAboutInled() {
6+
const t = useTranslations("landing");
7+
48
return (
59
<section id="about" className="py-24 px-6 bg-white">
610
<div className="max-w-4xl mx-auto text-center">
@@ -9,22 +13,22 @@ export function LandingAboutInled() {
913
alt="Inled Group Logo"
1014
className="h-20 mx-auto mb-10 grayscale hover:grayscale-0 transition-all duration-500"
1115
/>
12-
<h2 className="text-2xl md:text-3xl font-bold mb-6">Acerca de Inled Group</h2>
16+
<h2 className="text-2xl md:text-3xl font-bold mb-6">{t("about.title")}</h2>
1317
<p className="text-lg text-text-secondary leading-relaxed mb-8">
14-
Inled Group es una marca de software enfocada en la privacidad, la soberanía tecnológica Española y la IA local. Usamos tecnologías libres, agentes de IA para desarrollar y somos fans de Linux.
18+
{t("about.description")}
1519
</p>
1620
<div className="flex justify-center gap-8">
1721
<div className="flex flex-col items-center">
1822
<span className="text-3xl font-black text-primary">100%</span>
19-
<span className="text-xs uppercase tracking-widest font-bold opacity-50">Privado</span>
23+
<span className="text-xs uppercase tracking-widest font-bold opacity-50">{t("about.stats.private")}</span>
2024
</div>
2125
<div className="flex flex-col items-center">
2226
<span className="text-3xl font-black text-primary">Libre</span>
23-
<span className="text-xs uppercase tracking-widest font-bold opacity-50">Open Source</span>
27+
<span className="text-xs uppercase tracking-widest font-bold opacity-50">{t("about.stats.openSource")}</span>
2428
</div>
2529
<div className="flex flex-col items-center">
2630
<span className="text-3xl font-black text-primary">ES</span>
27-
<span className="text-xs uppercase tracking-widest font-bold opacity-50">Made in Spain</span>
31+
<span className="text-xs uppercase tracking-widest font-bold opacity-50">{t("about.stats.madeInSpain")}</span>
2832
</div>
2933
</div>
3034
</div>

components/landing/comparison.tsx

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,86 @@
11
"use client";
22

3+
import { useTranslations } from "next-intl";
34
import { Check, X, Star } from "lucide-react";
45

56
export function LandingComparison() {
7+
const t = useTranslations("landing");
8+
69
const suites = [
710
{
811
name: "InSuite",
912
logo: "/logo.svg",
10-
privacy: "Local",
13+
privacy: t("comparison.privacy.local"),
1114
auditable: true,
1215
comp: 5,
1316
install: false,
14-
price: "Gratis",
15-
diff: "Soporta Export PDF, IndexedDB, No es Chino",
17+
price: t("comparison.price.free"),
18+
diff: t("comparison.suites.insuite.diff"),
1619
featured: true,
1720
},
1821
{
1922
name: "Ziziyi Office",
2023
logo: "https://hosted.inled.es/ziziyi-office-insuite.png",
21-
privacy: "Local",
24+
privacy: t("comparison.privacy.local"),
2225
auditable: true,
2326
comp: 5,
2427
install: false,
25-
price: "Freemium",
26-
diff: "Sujeto a normativa china. No puedes exportar a PDF. Sin guardado en navegador.",
28+
price: t("comparison.price.freemium"),
29+
diff: t("comparison.suites.ziziyi.diff"),
2730
},
2831
{
2932
name: "OnlyOffice",
3033
logo: "https://hosted.inled.es/onlyoffice-insuite.png",
31-
privacy: "Nube/Self-hosted",
34+
privacy: t("comparison.privacy.cloud"),
3235
auditable: false,
3336
comp: 5,
3437
install: true,
35-
price: "Freemium",
36-
diff: "Ciertos componentes no son auditables. Relacionado con Rusia",
38+
price: t("comparison.price.freemium"),
39+
diff: t("comparison.suites.onlyoffice.diff"),
3740
},
3841
{
3942
name: "LibreOffice",
4043
logo: "https://hosted.inled.es/libreoffice-insuite.png",
41-
privacy: "Local",
44+
privacy: t("comparison.privacy.local"),
4245
auditable: true,
4346
comp: 3,
4447
install: true,
45-
price: "Gratis",
46-
diff: "Interfaz pésima, necesidad de instalación, baja compatibilidad con Office",
48+
price: t("comparison.price.free"),
49+
diff: t("comparison.suites.libreoffice.diff"),
4750
},
4851
{
4952
name: "MS Office",
5053
logo: "https://hosted.inled.es/microslop-insuite.png",
51-
privacy: "Baja (Cloud)",
54+
privacy: t("comparison.privacy.lowCloud"),
5255
auditable: false,
5356
comp: 5,
5457
install: true,
55-
price: "Suscripción",
56-
diff: "Propietario, caro, espionaje de datos",
58+
price: t("comparison.price.subscription"),
59+
diff: t("comparison.suites.msoffice.diff"),
5760
},
5861
];
5962

6063
return (
6164
<section className="py-24 px-6 bg-white overflow-x-auto">
6265
<div className="max-w-7xl mx-auto">
6366
<div className="text-center mb-16">
64-
<h2 className="text-3xl md:text-5xl font-extrabold mb-4">Comparativa honesta</h2>
67+
<h2 className="text-3xl md:text-5xl font-extrabold mb-4">{t("comparison.title")}</h2>
6568
<p className="text-text-secondary text-lg max-w-2xl mx-auto">
66-
La mejor forma de que veas por qué InSuite es la mejor opción es comparándolo con el resto de opciones del mercado.
69+
{t("comparison.subtitle")}
6770
</p>
6871
</div>
6972

7073
<div className="min-w-[1000px]">
7174
<table className="w-full border-collapse">
7275
<thead>
7376
<tr className="border-b-2 border-zinc-100 text-left">
74-
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">Suite</th>
75-
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">Privacidad</th>
76-
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">Auditable</th>
77-
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">Compatibilidad</th>
78-
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">Sin Instalar</th>
79-
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">Precio</th>
80-
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">Diferencias clave</th>
77+
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">{t("comparison.headers.suite")}</th>
78+
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">{t("comparison.headers.privacy")}</th>
79+
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">{t("comparison.headers.auditable")}</th>
80+
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">{t("comparison.headers.compatibility")}</th>
81+
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">{t("comparison.headers.noInstall")}</th>
82+
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">{t("comparison.headers.price")}</th>
83+
<th className="py-6 px-4 font-bold text-zinc-400 uppercase text-xs">{t("comparison.headers.keyDiffs")}</th>
8184
</tr>
8285
</thead>
8386
<tbody>
@@ -103,7 +106,7 @@ export function LandingComparison() {
103106
</div>
104107
</td>
105108
<td className="py-6 px-4">
106-
<span className={`px-3 py-1 rounded-full text-xs font-bold whitespace-nowrap ${suite.privacy.includes('100%') ? 'bg-green-100 text-green-700' : 'bg-zinc-100 text-zinc-600'}`}>
109+
<span className={`px-3 py-1 rounded-full text-xs font-bold whitespace-nowrap ${suite.privacy.includes('100%') || suite.privacy === t("comparison.privacy.local") ? 'bg-green-100 text-green-700' : 'bg-zinc-100 text-zinc-600'}`}>
107110
{suite.privacy}
108111
</span>
109112
</td>
@@ -121,7 +124,7 @@ export function LandingComparison() {
121124
{!suite.install ? <Check className="w-5 h-5 text-green-500 mx-auto" /> : <X className="w-5 h-5 text-red-500 mx-auto" />}
122125
</td>
123126
<td className="py-6 px-4">
124-
<span className={suite.price === 'Gratis' ? 'text-green-600 font-bold' : 'text-zinc-600'}>{suite.price}</span>
127+
<span className={suite.price === t("comparison.price.free") ? 'text-green-600 font-bold' : 'text-zinc-600'}>{suite.price}</span>
125128
</td>
126129
<td className="py-6 px-4 text-sm text-text-secondary italic">
127130
{suite.diff}

components/landing/editors.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
11
"use client";
22

3+
import { useTranslations } from "next-intl";
34
import { FileText, FileSpreadsheet, Presentation, Puzzle } from "lucide-react";
45

56
export function LandingEditors() {
7+
const t = useTranslations("landing");
8+
69
const editors = [
710
{
8-
name: "Documentos",
9-
desc: "Compatible con .docx. La misma potencia que Microsoft Word, pero mil veces mejor que Google Docs.",
11+
name: t("editors.docx.name"),
12+
desc: t("editors.docx.desc"),
1013
icon: FileText,
1114
color: "border-blue-200",
1215
iconColor: "text-blue-600",
1316
bgColor: "bg-blue-50",
1417
},
1518
{
16-
name: "Hojas de Cálculo",
17-
desc: "Compatible con .xlsx. Realiza análisis complejos y fórmulas con total compatibilidad.",
19+
name: t("editors.xlsx.name"),
20+
desc: t("editors.xlsx.desc"),
1821
icon: FileSpreadsheet,
1922
color: "border-green-200",
2023
iconColor: "text-green-600",
2124
bgColor: "bg-green-50",
2225
},
2326
{
24-
name: "Presentaciones",
25-
desc: "Compatible con .pptx. Crea slides impactantes directamente en tu navegador.",
27+
name: t("editors.pptx.name"),
28+
desc: t("editors.pptx.desc"),
2629
icon: Presentation,
2730
color: "border-orange-200",
2831
iconColor: "text-orange-600",
@@ -34,9 +37,9 @@ export function LandingEditors() {
3437
<section id="editors" className="py-24 px-6 bg-zinc-50">
3538
<div className="max-w-7xl mx-auto">
3639
<div className="text-center mb-16">
37-
<h2 className="text-3xl md:text-5xl font-extrabold mb-4">Herramientas Profesionales</h2>
40+
<h2 className="text-3xl md:text-5xl font-extrabold mb-4">{t("editors.title")}</h2>
3841
<p className="text-text-secondary text-lg max-w-2xl mx-auto">
39-
Editores de escritorio potentes, ahora en tu navegador. 100% compatibles con tus archivos de siempre.
42+
{t("editors.subtitle")}
4043
</p>
4144
</div>
4245

@@ -62,9 +65,9 @@ export function LandingEditors() {
6265
<div className="w-16 h-16 mb-6 bg-primary rounded-2xl flex items-center justify-center text-white group-hover:rotate-12 transition-transform shadow-lg shadow-primary/20">
6366
<Puzzle className="w-10 h-10" />
6467
</div>
65-
<h3 className="text-xl font-bold mb-3 text-primary">Plugins</h3>
68+
<h3 className="text-xl font-bold mb-3 text-primary">{t("editors.plugins.title")}</h3>
6669
<p className="text-text-secondary text-sm leading-relaxed">
67-
Traducción, IA local, corrección ortográfica y más. Personaliza tu oficina con nuestro catálogo.
70+
{t("editors.plugins.desc")}
6871
</p>
6972
</div>
7073
</div>

components/landing/embedded-editor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export function EmbeddedEditor({ fileUrl, fileType }: EmbeddedEditorProps) {
198198
<p className="text-xs text-text-secondary mt-2">Todo se ejecuta localmente en tu navegador</p>
199199
</div>
200200
)}
201-
<div id={placeholderId} className="w-full h-full" />
201+
{hasHydrated && <div id={placeholderId} className="w-full h-full" />}
202202
<iframe className="w-0 h-0 hidden" src={APP_ROOT + PRELOAD_HTML} />
203203
</div>
204204
);

0 commit comments

Comments
 (0)