Build This Now
Build This Now
O que é o Código Claude?Instalar o Claude CodeInstalador Nativo do Claude CodeO Teu Primeiro Projeto com Claude Code
Failed Payment RecoveryLocalization and i18nPDF InvoicesReferral ProgramTwo-Factor AuthNext.js DevTools MCPNext.js Agent SetupSubscription BillingMulti-Tenant SaaSAI Chat FeatureRate LimitingStripe WebhooksSemantic SearchRealtime UpdatesDrip Email Sequencesv2.1.122 Release NotesClaude Code Dynamic Workflows: Como Orquestrar 1.000 Subagentes Num Codebase RealMelhores Práticas do Claude CodeBoas Práticas para o Claude Opus 4.7Claude Code num VPSIntegração GitRevisão de Código com ClaudeWorktrees no Claude CodeControle Remoto do Claude CodeChannels do Claude CodeChannels, Routines, Teleport, DispatchTarefas Agendadas no Claude CodePermissões do Claude CodeModo Auto do Claude CodeAdicionar Pagamentos Stripe Com o Claude CodeFeedback LoopsFluxos de Trabalho com TodosTarefas no Claude CodeTemplates de ProjetoPreços e Consumo de Tokens no Claude CodePreços do Claude Code: O Que Vais Mesmo PagarClaude Code Ultra ReviewConstruir Uma App Next.js Com o Claude CodeSupabase DatabaseVercel DeepsecTest-Driven DevelopmentComo Construir um MVP de SaaS Com o Claude CodeAdicionar Autenticação Com o Claude Code (Supabase Auth)Adicionar Email Transacional Com o Claude Code (Resend + React Email)Construir Uma API Type-Safe Com o Claude Code (oRPC + Zod)File UploadsBackground Jobs (Inngest)Admin DashboardFull-Text SearchClaude Agent SDKKiro Migration GuideClaude Research AgentLeave Grok BuildRoute Subagent ModelsClaude Monorepo SetupCI Repair AgentVisual Regression TestsAgent Cost DashboardCursor Migration GuideComércio Agêntico: Como Construir uma App Que Agentes de IA Podem Pagar1M ContextUser API KeysAudit LogsCSV Import PipelineDatabase MigrationsProduction Error TrackingFeature FlagsGitHub ActionsHeadless ModeMax Plan vs APICaching and RevalidationIn-App NotificationsOutbound WebhooksPrompt CachingRoles & PermissionsMarketplace PaymentsUsage-Based BillingQuanto Custa Construir um SaaS com Claude Code em 2026Parallel AI AgentsCoding Agent Injection
speedy_devvkoen_salo
Blog/Handbook/Workflow/Localization and i18n

Localization and i18n

A complete Next.js i18n setup with next-intl: locale-routed segments, server-component message loading, hreflang tags, locale-aware Resend emails, and a Claude Code subagent that translates your catalogs on commit.

Quer o framework por trás destes projetos?

Obtenha o sistema Claude Code que usamos para planejar, construir, testar e lançar software em produção.

Veja o que construímos para empresas →
speedy_devvkoen_salo
speedy_devvWritten by speedy_devvPublished Jul 30, 20269 min readHandbook hubWorkflow index

A locale-routed Next.js 16 app is mostly plumbing: one [locale] segment, one request config, one proxy file. The hard part is keeping four message catalogs and a folder of MDX posts in sync while the English copy changes every day. This guide wires the plumbing with next-intl, then hands the sync problem to a Claude Code subagent that runs on commit and is policed by a script that fails the build when a key goes missing.

The result: /pricing, /fr/pricing, /de/pricing, /ja/pricing, correct hreflang on every page, and welcome emails that arrive in the language the user signed up in.

What ships

Everything lives in the repo. The catalogs are plain JSON, the agent definition is a Markdown file, and the gate is one Node script with no dependencies.

your-app/
├── .claude/agents/catalog-translator.md
├── .husky/pre-commit
├── messages/{en,fr,de,ja}.json
├── scripts/check-catalogs.mjs
└── src/
    ├── app/[locale]/{layout.tsx,pricing/page.tsx}
    ├── app/sitemap.ts
    ├── components/plan-picker.tsx
    ├── i18n/{routing,navigation,request}.ts
    ├── lib/{seo.ts,posts.ts,email.tsx}
    └── proxy.ts

Install and wire the plugin

next-intl ships a Next.js plugin that tells the bundler where your request config lives. Install it alongside Resend, which the email section needs. Then wrap your config, turning on cacheComponents so the "use cache" directive is available later.

npm install next-intl resend
// next.config.ts
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";

const nextConfig: NextConfig = { cacheComponents: true };

export default createNextIntlPlugin()(nextConfig);

Define routing once

Every other file derives from this one. defineRouting is the single source of truth for which locales exist and how they appear in URLs. Export a Locale type from it so nothing in the app has to accept a raw string.

// src/i18n/routing.ts
import { defineRouting } from "next-intl/routing";

export const routing = defineRouting({
  locales: ["en", "fr", "de", "ja"],
  defaultLocale: "en",
  localePrefix: "as-needed",
});

export type Locale = (typeof routing.locales)[number];

localePrefix: "as-needed" keeps English at /pricing and prefixes everything else, which avoids a redirect hop on your highest-traffic pages. The cost is that you can no longer build URLs by string concatenation, which is why the next file matters.

createNavigation returns locale-aware versions of the Next.js navigation APIs. Use these instead of next/link and next/navigation so the prefix rule is applied for you.

// src/i18n/navigation.ts
import { createNavigation } from "next-intl/navigation";
import { routing } from "./routing";

export const { Link, redirect, usePathname, useRouter, getPathname } =
  createNavigation(routing);

getPathname is the useful one for SEO and emails. Give it a href and a locale and it returns the real path for that locale, prefix rules included.

Load messages on the server

getRequestConfig runs per request. It resolves the locale, validates it against the routing config, and dynamic-imports only that locale's catalog. Nothing else ships. Pin timeZone here too, or server and client can format the same date differently and trigger a hydration warning that only shows up in production.

// src/i18n/request.ts
import { hasLocale } from "next-intl";
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";

export default getRequestConfig(async ({ requestLocale }) => {
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested)
    ? requested
    : routing.defaultLocale;

  return {
    locale,
    messages: (await import(`../../messages/${locale}.json`)).default,
    timeZone: "UTC",
  };
});

Next.js 16 renamed middleware.ts to proxy.ts. The next-intl factory is unchanged, so the only difference from a Next.js 15 project is the filename.

// src/proxy.ts
import createMiddleware from "next-intl/middleware";
import { routing } from "@/i18n/routing";

export default createMiddleware(routing);

export const config = { matcher: "/((?!api|_next|_vercel|.*\\..*).*)" };

That matcher skips API routes, build assets, and anything with a file extension. Without the last clause the proxy prefixes /favicon.ico and you get a 404 you will spend twenty minutes on.

The locale segment

Move app/ under app/[locale]/. The layout awaits params (required in Next.js 16), rejects unknown locales, and opts every locale into static rendering via generateStaticParams plus setRequestLocale.

// src/app/[locale]/layout.tsx
import { NextIntlClientProvider, hasLocale } from "next-intl";
import { setRequestLocale } from "next-intl/server";
import { notFound } from "next/navigation";
import { routing } from "@/i18n/routing";

export function generateStaticParams() {
  return routing.locales.map((locale) => ({ locale }));
}

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  if (!hasLocale(routing.locales, locale)) notFound();
  setRequestLocale(locale);

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={null}>{children}</NextIntlClientProvider>
      </body>
    </html>
  );
}

Skip setRequestLocale and every page silently falls back to dynamic rendering. The build output is where you catch it.

The messages={null} prop is deliberate. Rendered from a Server Component, NextIntlClientProvider inherits locale, messages, now, timeZone, and formats from your request config, so a bare provider at the root ships the entire catalog to the browser. Opting out here and re-adding a narrowed provider further down the tree is what keeps the client bundle small.

Pages read messages with getTranslations, which is awaitable and runs entirely on the server: no provider, no client bundle, no flash of untranslated content. The call inside generateMetadata takes an explicit locale, because metadata generation can run outside the request scope setRequestLocale establishes.

// src/app/[locale]/pricing/page.tsx
import type { Metadata } from "next";
import { NextIntlClientProvider } from "next-intl";
import { getMessages, getTranslations, setRequestLocale } from "next-intl/server";
import { PlanPicker } from "@/components/plan-picker";
import { alternatesFor } from "@/lib/seo";
import type { Locale } from "@/i18n/routing";

type Props = { params: Promise<{ locale: Locale }> };

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: "Pricing" });
  return {
    title: t("title"),
    description: t("subtitle"),
    alternates: alternatesFor("/pricing", locale),
  };
}

export default async function PricingPage({ params }: Props) {
  const { locale } = await params;
  setRequestLocale(locale);

  const t = await getTranslations("Pricing");
  const messages = await getMessages();

  return (
    <main className="mx-auto max-w-2xl px-6 py-16">
      <h1 className="text-3xl font-semibold">{t("title")}</h1>
      <p className="mt-2 text-muted-foreground">{t("subtitle")}</p>
      <NextIntlClientProvider messages={{ Pricing: messages.Pricing }}>
        <PlanPicker />
      </NextIntlClientProvider>
    </main>
  );
}

That provider call is the whole client-bundle story. Because the root provider opted out, this narrowed messages object is the only thing that reaches the browser: one namespace instead of the entire catalog.

Client components use the hook instead. The plural below is ICU syntax that next-intl resolves against the active locale, not a string you assemble by hand.

// src/components/plan-picker.tsx
"use client";

import { useState } from "react";
import { useTranslations } from "next-intl";

export function PlanPicker() {
  const t = useTranslations("Pricing");
  const [seats, setSeats] = useState(1);

  return (
    <div className="mt-8 rounded-xl border p-6">
      <label htmlFor="seats">{t("seatsLabel")}</label>
      <input
        id="seats"
        type="number"
        min={1}
        value={seats}
        onChange={(e) => setSeats(Number(e.target.value))}
        className="mt-2 block w-24 rounded-md border px-3 py-2"
      />
      <p className="mt-3">{t("seats", { count: seats })}</p>
      <button className="mt-6 rounded-md border px-4 py-2">{t("cta")}</button>
    </div>
  );
}

Keys are stable identifiers, never English sentences, because English sentences change and then every translation silently detaches from its source.

{
  "Pricing": {
    "title": "Simple pricing",
    "subtitle": "One plan. Cancel anytime.",
    "seatsLabel": "Team size",
    "seats": "{count, plural, one {# seat} other {# seats}}",
    "cta": "Start free trial"
  },
  "Email": {
    "welcome": {
      "subject": "Welcome to Acme",
      "heading": "You are in",
      "body": "Your workspace is ready.",
      "cta": "Open dashboard"
    }
  }
}

hreflang without hand-maintained URL maps

Every localized page needs a canonical, one alternate per locale, and an x-default. Building those by concatenation breaks under as-needed prefixing, so derive them from getPathname.

// src/lib/seo.ts
import { getPathname } from "@/i18n/navigation";
import { routing, type Locale } from "@/i18n/routing";

const SITE = "https://acme.com";
type Href = Parameters<typeof getPathname>[0]["href"];

export function alternatesFor(href: Href, locale: Locale) {
  const url = (target: Locale) => SITE + getPathname({ href, locale: target });
  return {
    canonical: url(locale),
    languages: {
      ...Object.fromEntries(routing.locales.map((l) => [l, url(l)])),
      "x-default": url(routing.defaultLocale),
    },
  };
}

The sitemap reuses the same helper, so the two can never disagree. Next.js emits the xhtml:link alternates automatically once you supply alternates.languages.

// src/app/sitemap.ts
import type { MetadataRoute } from "next";
import { routing } from "@/i18n/routing";
import { alternatesFor } from "@/lib/seo";

const ROUTES = ["/", "/pricing", "/docs"] as const;

export default function sitemap(): MetadataRoute.Sitemap {
  return ROUTES.map((href) => {
    const { canonical, languages } = alternatesFor(href, routing.defaultLocale);
    return { url: canonical, lastModified: new Date(), alternates: { languages } };
  });
}

The translation subagent

Now the part that saves real time. A Claude Code subagent gets its own context window and its own tool allowlist, which suits a job that touches many files and should never touch application code.

The prompt is mostly prohibitions. Translation quality is rarely the failure mode; structural damage is. A model that helpfully reorders keys, drops an ICU placeholder, or translates a frontmatter field name breaks the build in a way that is annoying to trace.

---
name: catalog-translator
description: Translates message catalogs and English MDX posts into every non-source locale. Use after messages/en.json or an unsuffixed .mdx under content/ changes.
tools: Read, Write, Edit, Glob, Grep
model: sonnet
---

You translate product copy. You never write or modify application code.

Sources: `messages/en.json`, and any `content/**/*.mdx` without a locale
suffix. Target locales come from `src/i18n/routing.ts`, minus the default.

Hard rules, in priority order:

1. Never add, remove, rename, or reorder a JSON key. The target catalog has
   exactly the key set of `messages/en.json`, in the same order.
2. Never alter ICU syntax. `{count, plural, one {# seat} other {# seats}}`
   keeps its argument name, keywords, and `#` markers. Only the words inside
   the branches change.
3. Never translate code fences, inline code, URLs, file paths, JSX tag names,
   imports, or MDX component names.
4. In MDX frontmatter, translate only `title`, `description`, and the `faq`
   `question` and `answer` values. Copy every other field as-is.
5. Keep product names in English unless `messages/glossary.json` says
   otherwise. Use formal de (Sie) and polite ja (desu/masu).

Write only to `messages/<locale>.json` and `content/**/<name>.<locale>.mdx`.
If a string is ambiguous, translate it literally and log it under `_review`
in `messages/glossary.json` rather than guessing at intent.

The glossary is what turns this from a one-shot translator into something consistent across months of commits. Terms you decided not to translate, or translated a specific way, live there and are read on every run.

The deterministic gate

The subagent is the productive half. This script is the half you trust. It flattens every catalog, diffs the key sets against the source, and compares ICU argument names per key.

// scripts/check-catalogs.mjs
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";

const DIR = path.join(process.cwd(), "messages");
const SOURCE = "en";

function flatten(value, prefix = "") {
  const out = {};
  for (const [key, entry] of Object.entries(value)) {
    const next = prefix ? `${prefix}.${key}` : key;
    if (entry && typeof entry === "object") Object.assign(out, flatten(entry, next));
    else out[next] = String(entry);
  }
  return out;
}

function args(message) {
  const found = new Set();
  for (const m of message.matchAll(/\{\s*([A-Za-z0-9_]+)/g)) found.add(m[1]);
  return [...found].sort().join(",") || "none";
}

const load = async (locale) =>
  flatten(JSON.parse(await readFile(path.join(DIR, `${locale}.json`), "utf8")));

const skip = new Set(["glossary.json", `${SOURCE}.json`]);
const files = (await readdir(DIR)).filter((f) => f.endsWith(".json") && !skip.has(f));
const base = await load(SOURCE);
const errors = [];

for (const file of files) {
  const locale = path.basename(file, ".json");
  const target = await load(locale);

  for (const key of Object.keys(base)) {
    if (!(key in target)) errors.push(`${locale}: missing "${key}"`);
    else if (args(base[key]) !== args(target[key]))
      errors.push(`${locale}: "${key}" wants ${args(base[key])}, has ${args(target[key])}`);
  }
  for (const key of Object.keys(target))
    if (!(key in base)) errors.push(`${locale}: unknown "${key}"`);
}

if (errors.length > 0) {
  console.error(errors.join("\n"));
  process.exit(1);
}
console.log(`Catalogs match ${SOURCE}.json across ${files.length} locales.`);

Register it beside the other gates so it runs in the same breath as type checking.

{
  "scripts": {
    "check:catalogs": "node scripts/check-catalogs.mjs",
    "check": "npm run check:catalogs && tsc --noEmit && eslint . && next build"
  }
}

Translate on commit

The hook detects staged source files, invokes Claude Code headlessly against the subagent, verifies the output, and stages the result. If nothing translatable changed, it runs the fast check and exits.

#!/usr/bin/env sh
# .husky/pre-commit

CHANGED=$(git diff --cached --name-only --diff-filter=ACM \
  | grep -E '^(messages/en\.json|content/.+\.mdx)$' \
  | grep -Ev '\.[a-z]{2}\.mdx$')

if [ -z "$CHANGED" ]; then
  npm run check:catalogs
  exit $?
fi

claude -p "Use the catalog-translator subagent to sync every non-source locale
for these changed files:

$CHANGED

Read target locales from src/i18n/routing.ts. Write only under messages/ and content/." \
  --allowedTools "Task,Read,Write,Edit,Glob,Grep" \
  --permission-mode acceptEdits

npm run check:catalogs || exit 1

git add messages content

Task has to be in that list, otherwise the outer run cannot delegate to the subagent and just does the work itself with whatever tools it has. Scope it to Task(catalog-translator) if you want the run to be able to call that one agent and nothing else.

Two honest caveats. Headless runs go through the same Anthropic account as your interactive sessions, so a busy day of copy edits is not free. And a network round trip in front of every commit is a real tax; if that bothers you, keep only npm run check:catalogs in the hook and move the claude -p call to a pull request job. The gate is the cheap part, and the gate is what protects the build.

Locale-aware Resend emails

Emails render outside the request scope, so React hooks and getTranslations are unavailable. createTranslator is the escape hatch: give it a locale and a messages object and it returns the same t function, synchronously.

Store the user's locale at signup: read it from the route segment with getLocale(), persist it on the profile row in Supabase, then read it back whenever you send. The send function pulls that locale's catalog, builds a locale-correct CTA URL with getPathname, and sets Content-Language so mail clients render it the right way.

// src/lib/email.tsx
import { createTranslator } from "next-intl";
import { Resend } from "resend";
import { getPathname } from "@/i18n/navigation";
import type { Locale } from "@/i18n/routing";

const resend = new Resend(process.env.RESEND_API_KEY);
const SITE = "https://acme.com";

export async function sendWelcomeEmail(to: string, locale: Locale) {
  const messages = (await import(`../../messages/${locale}.json`)).default;
  const t = createTranslator({ locale, messages, namespace: "Email.welcome" });
  const href = SITE + getPathname({ href: "/dashboard", locale });

  return resend.emails.send({
    from: "Acme <hello@acme.com>",
    to,
    subject: t("subject"),
    headers: { "Content-Language": locale },
    react: (
      <html lang={locale}>
        <body style={{ fontFamily: "system-ui, sans-serif", padding: 24 }}>
          <h1 style={{ fontSize: 20 }}>{t("heading")}</h1>
          <p style={{ fontSize: 14, lineHeight: 1.6 }}>{t("body")}</p>
          <a href={href}>{t("cta")}</a>
        </body>
      </html>
    ),
  });
}

Because the copy lives in messages/*.json alongside the UI strings, the same subagent and the same parity check cover transactional email. Add a key under Email, commit, and all four locales arrive together or the commit fails.

Cache the content index per locale

Listing the MDX directory is work you want done once per locale, not once per request. A "use cache" function with a per-locale tag gives you that, and a content deploy can invalidate one language without touching the rest.

// src/lib/posts.ts
import { readdir } from "node:fs/promises";
import path from "node:path";
import { cacheLife, cacheTag } from "next/cache";
import { routing, type Locale } from "@/i18n/routing";

export async function getPostSlugs(locale: Locale) {
  "use cache";
  cacheLife("hours");
  cacheTag(`posts-${locale}`);

  const files = await readdir(path.join(process.cwd(), "content", "blog"));
  const isDefault = locale === routing.defaultLocale;
  const suffix = isDefault ? ".mdx" : `.${locale}.mdx`;

  return files
    .filter((f) => f.endsWith(suffix))
    .filter((f) => !isDefault || !/\.[a-z]{2}\.mdx$/.test(f))
    .map((f) => f.replace(suffix, ""));
}

That second filter is the one people forget: a .mdx ending also matches post.fr.mdx, so the English index quietly fills with French posts.

The shape of the whole thing

Three files configure routing. One layout awaits params and pins the locale. One helper derives hreflang from the routing config so canonicals and sitemaps cannot drift. One subagent does the translation, and one dependency-free script decides whether its output is allowed into the repo. The agent is fast and occasionally wrong; the script is the reason that is fine.

Making a workflow like this hold up is less about the individual prompt and more about the harness around it: plan before building, evaluate the output, test it, gate the merge. That is what the $29 Code Kit is, a one-time purchase with no subscription that sits on top of Claude Code and runs a feature through plan, build, evaluate, and test, exiting only on zero type errors, zero lint errors, and a clean build. Claude Code itself still needs a paid Anthropic plan, and the Kit does not change that. It changes what happens between "translate this" and "this is safe to merge."


Posted by @speedy_devv

Continue in Workflow

  • Comércio Agêntico: Como Construir uma App Que Agentes de IA Podem Pagar
    Um guia em português simples sobre comércio agêntico em 2026: o que fazem o x402, o ACP e o Machine Payments Protocol, mais um passo a passo de fim de semana para lançar uma API paga que agentes de IA podem comprar.
  • Melhores Práticas do Claude Code
    Cinco hábitos separam os engenheiros que entregam com Claude Code: PRDs, regras modulares em CLAUDE.md, slash commands personalizados, resets com /clear e uma mentalidade de evolução do sistema.
  • Modo Auto do Claude Code
    Um segundo modelo Sonnet revê cada chamada de ferramenta do Claude Code antes de ser executada. O que o modo auto bloqueia, o que permite e as regras de permissão que cria nas tuas definições.
  • Channels, Routines, Teleport, Dispatch
    As quatro funcionalidades de Claude Code que a Anthropic lançou em março e abril de 2026 e que transformam a CLI numa camada de coordenação orientada a eventos entre telemóvel, web e desktop.
  • Claude Code 1M Context in Practice: When Bigger Isn't Better
    The 1M-token context window is GA at flat pricing, but bigger isn't always better. A decision framework, token-cost math, and when to use /compact, subagents, and dynamic workflows instead.
  • How to Build an Admin Dashboard With Claude Code
    Ship an internal admin panel with Claude Code: role-gated routes, a searchable users and orders table with pagination, impersonation-safe RLS, and metrics tiles pulled through a type-safe API.

More from Handbook

  • Fundamentos do agente
    Cinco maneiras de criar agentes especializados no Código Claude: Sub-agentes de tarefas, .claude/agents YAML, comandos de barra personalizados, personas CLAUDE.md e prompts de perspetiva.
  • Engenharia de Harness para Agentes
    O harness é cada camada ao redor do seu agente de IA, exceto o modelo em si. Aprenda os cinco pontos de controle, o paradoxo das restrições, e por que o design do harness determina o desempenho do agente mais do que o modelo.
  • Padrões de Agentes
    Orchestrator, fan-out, cadeia de validação, routing especializado, refinamento progressivo e watchdog. Seis formas de orquestração para ligar sub-agentes no Claude Code.
  • Boas Práticas para Equipas de Agentes
    Padrões testados em produção para Equipas de Agentes Claude Code. Prompts de criação ricos em contexto, tarefas bem dimensionadas, posse de ficheiros, modo delegado, e correções das versões v2.1.33-v2.1.45.

Quer o framework por trás destes projetos?

Obtenha o sistema Claude Code que usamos para planejar, construir, testar e lançar software em produção.

Veja o que construímos para empresas →
speedy_devvkoen_salo

Failed Payment Recovery

Build Stripe failed payment recovery end to end: an invoice.payment_failed webhook, a dunning email sequence running as a durable Inngest schedule with Resend, a card-update portal link, a grace period, and automatic entitlement downgrade.

PDF Invoices

Generate a branded PDF invoice in Next.js the moment Stripe fires invoice.paid: render with React-PDF inside an Inngest step, store it in a private Supabase bucket, email it through Resend, and serve billing history behind signed URLs.

On this page

What ships
Install and wire the plugin
Define routing once
Load messages on the server
The locale segment
hreflang without hand-maintained URL maps
The translation subagent
The deterministic gate
Translate on commit
Locale-aware Resend emails
Cache the content index per locale
The shape of the whole thing

Quer o framework por trás destes projetos?

Obtenha o sistema Claude Code que usamos para planejar, construir, testar e lançar software em produção.

Veja o que construímos para empresas →