瀏覽代碼

feat(locales): add portuguese language (#76)

Lucas Neves Pereira 1 月之前
父節點
當前提交
43951eeadb
共有 5 個文件被更改,包括 445 次插入3 次删除
  1. 5 1
      locales/client.ts
  2. 434 0
      locales/pt.ts
  3. 1 0
      locales/server.ts
  4. 1 1
      middleware.ts
  5. 4 1
      src/widgets/language-selector/language-selector.tsx

+ 5 - 1
locales/client.ts

@@ -3,7 +3,7 @@
 import { createI18nClient } from "next-international/client";
 
 // NOTE: Also update middleware.ts to support locale
-export const languages = ["en", "fr", "es", "zh-CN", "ru"];
+export const languages = ["en", "fr", "es", "zh-CN", "ru", "pt"];
 
 export const { useI18n, useScopedI18n, I18nProviderClient, useChangeLocale, defineLocale, useCurrentLocale } = createI18nClient(
   {
@@ -27,6 +27,10 @@ export const { useI18n, useScopedI18n, I18nProviderClient, useChangeLocale, defi
       await new Promise((resolve) => setTimeout(resolve, 100));
       return import("./ru");
     },
+    pt: async () => {
+      await new Promise((resolve) => setTimeout(resolve, 100));
+      return import("./pt");
+    },
   },
   {
     // Uncomment to set base path

+ 434 - 0
locales/pt.ts

@@ -0,0 +1,434 @@
+export default {
+  email_sent: "Email enviado",
+  cant_send_email: "Não foi possível enviar o email",
+  logout: "Terminar sessão",
+  verify_email: "Verifique o seu email",
+  verify_email_subtitle: "Por favor, verifique o seu email para continuar.",
+  resend_email: "Reenviar email",
+  resend_email_countdown: "Reenviar email em {seconds} segundos",
+  signin_error_subtitle: "Por favor, verifique as suas credenciais e tente novamente.",
+  register_title: "Criar conta",
+  register_description: "Insira os seus dados abaixo para criar uma conta",
+  register_terms: "Ao registar-se, concorda com os nossos",
+  register_privacy: "Termos e Condições",
+  register_privacy_link: "e com a nossa",
+  register_privacy_link_2: "Política de Privacidade",
+  password_forgot_title: "Esqueceu-se da palavra-passe?",
+  password_forgot_subtitle: "Insira o seu email para redefinir a palavra-passe",
+  new_password: "Nova palavra-passe",
+  new_password_placeholder: "Insira a nova palavra-passe",
+  current_password: "Palavra-passe atual",
+  current_password_placeholder: "Insira a palavra-passe atual",
+  confirm_password: "Confirmar palavra-passe",
+  confirm_password_placeholder: "Confirme a palavra-passe",
+
+  success: {
+    feedback_sent: "Feedback enviado",
+    password_forgot_success: "Email enviado",
+    reset_password_success: "Palavra-passe redefinida com sucesso",
+    password_updated_successfully: "Palavra-passe atualizada com sucesso",
+  },
+
+  error: {
+    invalid_credentials: "Credenciais inválidas ou conta inexistente",
+    upload_failed: "Falha ao enviar ficheiro",
+    generic_error: "Erro durante a operação",
+    sending_email: "Erro ao enviar email",
+  },
+
+  backend_errors: {
+    EMAIL_ALREADY_EXISTS: "Este email já está registado",
+    INVALID_FILE_TYPE: "Tipo de ficheiro inválido",
+    FILE_TOO_LARGE: "Ficheiro demasiado grande",
+    NO_FILE_UPLOADED: "Nenhum ficheiro enviado",
+    IMAGE_PROCESSING_ERROR: "Erro no processamento da imagem",
+    upload_failed: "Falha no envio",
+  },
+
+  profile: {
+    new_workout: "Novo treino",
+    alert: {
+      title: "O seu progresso está guardado no navegador.",
+      create_account: "Crie uma conta",
+      log_in: "Inicie sessão",
+      to_ensure_it_is_not_getting_lost: "para garantir que não se perde.",
+    },
+  },
+
+  // Release Notes
+  release_notes: {
+    title: "Novidades",
+    release_notes: "Notas de Lançamento",
+    notes: {
+      note_2025_06_19: {
+        title: "📱 Agora disponível como PWA!",
+        content:
+          "O Workout.cool v1.2 já é uma Progressive Web App! Instale-a no seu telemóvel para uma experiência de aplicação nativa com acesso offline. 🚀",
+      },
+      note_2025_06_18: {
+        title: "🚀 Nº 1 em destaque no Hacker News!",
+        content:
+          "O Workout.cool chegou ao primeiro lugar no Hacker News! Obrigado a todos pelo apoio incrível e bem-vindos todos os novos utilizadores! 💪",
+      },
+      note_2025_06_01: {
+        title: "🎉 Novo: Dialogo de Notas de Lançamento",
+        content:
+          "Agora pode ver as novidades diretamente no cabeçalho! Fique atento a mais atualizações.",
+      },
+      note_2025_05_20: {
+        title: "Melhorias na UI",
+        content:
+          "Responsividade móvel aprimorada e efeitos subtis ao passar o cursor sobre botões.",
+      },
+    },
+  },
+
+  // Contact Support
+  contact_support: "Contactar Suporte",
+  contact_support_subtitle:
+    "Descreva o seu problema e iremos ajudá-lo o mais rápido possível. Também pode escrever-nos diretamente para",
+
+  // Social Platforms
+  social_platforms: {
+    x: "X (Twitter)",
+    facebook: "Facebook",
+    email: "Email",
+    whatsapp: "WhatsApp",
+    website: "Website",
+    phone: "Telefone",
+    youtube: "YouTube",
+    linkedin: "LinkedIn",
+    snapchat: "Snapchat",
+    instagram: "Instagram",
+    tiktok: "TikTok",
+    threads: "Threads",
+  },
+
+  // Workout Builder
+  workout_builder: {
+    confirm_delete:
+      "Tem a certeza de que pretende eliminar esta sessão de treino?",
+    steps: {
+      equipment: {
+        title: "Equipamento",
+        description: "Selecione o seu equipamento",
+      },
+      muscles: {
+        title: "Músculos",
+        description: "Escolha a sua zona de treino",
+      },
+      exercises: {
+        title: "Exercícios",
+        description: "Personalize o seu treino",
+      },
+    },
+    muscles: {
+      back: "Costas",
+      abdominals: "Abdominais",
+      biceps: "Bíceps",
+      triceps: "Tríceps",
+      chest: "Peito",
+      shoulders: "Ombros",
+      quadriceps: "Quadríceps",
+      hamstrings: "Isquiotibiais",
+      glutes: "Glúteos",
+      calves: "Panturrilhas",
+      forearms: "Antebraços",
+      traps: "Trapézio",
+      obliques: "Oblíquos",
+    },
+    exercise: {
+      watch_video: "Ver vídeo",
+      shuffle: "Aleatorizar",
+      pick: "Escolher",
+      remove: "Remover",
+      no_video_available: "Vídeo indisponível.",
+    },
+    loading: {
+      exercises: "A carregar exercícios...",
+    },
+    error: {
+      loading_exercises: "Erro ao carregar exercícios",
+    },
+    no_exercises_found:
+      "Nenhum exercício encontrado. Experimente mudar a seleção de equipamento ou músculos.",
+    equipment: {
+      bodyweight: {
+        label: "Peso corporal",
+        description: "Exercícios apenas com o peso do corpo",
+      },
+      dumbbell: {
+        label: "Halteres",
+        description: "Exercícios com halteres",
+      },
+      barbell: {
+        label: "Barra",
+        description: "Movimentos compostos com barra",
+      },
+      kettlebell: {
+        label: "Kettlebell",
+        description: "Exercícios dinâmicos com kettlebell",
+      },
+      band: {
+        label: "Elástico",
+        description: "Exercícios com banda de resistência",
+      },
+      plate: {
+        label: "Placa",
+        description: "Exercícios usando discos de peso",
+      },
+      pullup_bar: {
+        label: "Barra de barra fixa",
+        description: "Exercícios para a parte superior do corpo com barra fixa",
+      },
+      bench: {
+        label: "Banco",
+        description: "Exercícios de banco e apoio",
+      },
+    },
+    navigation: {
+      previous: "Anterior",
+      continue: "Continuar",
+      complete: "Concluir",
+    },
+    stats: {
+      "muscle_selected#zero": "0 músculos selecionados",
+      "muscle_selected#one": "1 músculo selecionado",
+      "muscle_selected#other": "{count} músculos selecionados",
+      "equipment_selected#zero": "0 equipamentos selecionados",
+      "equipment_selected#one": "1 equipamento selecionado",
+      "equipment_selected#other": "{count} equipamentos selecionados",
+      selected: "Selecionado",
+      total: "Total",
+      equipment_ready: "equipamento pronto",
+      equipment_ready_plural: "equipamentos prontos",
+    },
+    selection: {
+      choose_your_arsenal: "Escolha o seu arsenal",
+      select_equipment_description:
+        "Selecione equipamento para desbloquear treinos personalizados",
+      clear_all: "Limpar tudo",
+      muscle_selection_coming_soon: "Seleção de músculos (Em breve)",
+      muscle_selection_description:
+        "Selecione o(s) músculo(s) que deseja treinar clicando neles.",
+      exercise_selection_coming_soon: "Seleção de exercícios (Em breve)",
+      exercise_selection_description:
+        "Nesta etapa verá recomendações de exercícios personalizadas.",
+    },
+    session: {
+      back_to_workout: "Voltar ao treino",
+      congrats: "Parabéns, treino concluído! 🎉",
+      congrats_subtitle: "Conseguiu!",
+      see_instructions: "Ver instruções",
+      finish_set: "Concluir série",
+      finish_session: "Terminar sessão",
+      bodyweight: "Peso corporal",
+      weight: "Peso",
+      reps: "Reps",
+      time: "Tempo",
+      next_exercise: "Próximo exercício",
+      add_set: "Adicionar série",
+      add_column: "Adicionar coluna",
+      add_row: "Adicionar linha",
+      remove_column: "Remover coluna",
+      set_number: "Série {number}",
+      set_number_plural: "Séries {number}",
+      set_number_singular: "Série {number}",
+      set_number_plural_singular: "Séries {number}",
+      workout_in_progress: "Treino em curso",
+      started_at: "Iniciado às",
+      quit_workout: "Abandonar treino",
+      elapsed_time: "Tempo decorrido",
+      chronometer: "Cronómetro",
+      exercise_progress: "Progresso do exercício",
+      total_volume: "Volume total",
+      current_exercise: "Exercício atual",
+      complete: "Concluído",
+      active: "Ativo",
+      already_have_a_active_session:
+        "Já tem uma sessão ativa. Não é possível repetir sem terminar ou abandonar o treino.",
+      no_exercise_selected: "Nenhum exercício selecionado",
+      quit_workout_title: "Abandonar treino?",
+      progress: "Progresso",
+      quit_warning:
+        "Tem a certeza de que pretende abandonar? Pode guardar o progresso ou perdê-lo completamente.",
+      save_and_quit: "Guardar e sair",
+      quit_without_save: "Sair sem guardar",
+      continue_workout: "Continuar treino",
+      history: "Histórico de treinos [{count}]",
+      no_workout_yet: "Ainda sem treinos.",
+      start: "iniciar",
+      end: "terminar",
+      exercise: "EXERCÍCIO",
+      repeat: "Repetir",
+      delete: "Eliminar",
+    },
+    attribute_value: {
+      bodyweight: "Peso corporal",
+      strength: "Força",
+      powerlifting: "Powerlifting",
+      calisthenic: "Calistenia",
+      plyometrics: "Pliometria",
+      stretching: "Alongamento",
+      strongman: "Strongman",
+      cardio: "Cardio",
+      stabilization: "Estabilização",
+      power: "Potência",
+      resistance: "Resistência",
+      crossfit: "CrossFit",
+      weightlifting: "Levantamento de peso",
+      neck: "Pescoço",
+      lats: "Dorsais",
+      adductors: "Adutores",
+      abductors: "Abdutores",
+      groin: "Virilha",
+      full_body: "Corpo inteiro",
+      rotator_cuff: "Manguito rotador",
+      hip_flexor: "Flexor da anca",
+      achilles_tendon: "Tendão de Aquiles",
+      fingers: "Dedos",
+      smith_machine: "Máquina Smith",
+      other: "Outro",
+      ez_bar: "Barra EZ",
+      machine: "Máquina",
+      desk: "Secretária",
+      none: "Nenhum",
+      cable: "Cabo",
+      medicine_ball: "Medicine ball",
+      swiss_ball: "Swiss ball",
+      foam_roll: "Rolo de espuma",
+      trx: "TRX",
+      box: "Caixa",
+      ropes: "Corda",
+      spin_bike: "Bicicleta de spinning",
+      step: "Step",
+      bosu: "BOSU",
+      tyre: "Pneu",
+      sandbag: "Saco de areia",
+      pole: "Barra vertical",
+      wall: "Parede",
+      bar: "Barra",
+      rack: "Rack",
+      car: "Carro",
+      sled: "Sledge",
+      chain: "Corrente",
+      skierg: "SkiErg",
+      rope: "Corda",
+      na: "N/D",
+      isolation: "Isolamento",
+      compound: "Composto",
+    },
+  },
+
+  commons: {
+    signup_with: "Inscrever com {provider}",
+    signin_with: "Entrar com {provider}",
+    signup: "Inscrever-se",
+    login: "Iniciar sessão",
+    connecting: "A ligar...",
+    login_to_your_account_title: "Inicie sessão na sua conta",
+    login_to_your_account_subtitle:
+      "Insira as suas credenciais abaixo para entrar",
+    password_forgot: "Esqueceu-se da palavra-passe?",
+    password_reset_success: "Palavra-passe redefinida com sucesso",
+    dont_have_account: "Ainda não tem conta?",
+    already_have_account: "Já tem uma conta?",
+    or: "Ou",
+    add: "Adicionar",
+    your_feminine: "a sua",
+    password: "Palavra-passe",
+    email: "Email",
+    logout: "Terminar sessão",
+    first_name: "Nome",
+    last_name: "Apelido",
+    verify_password: "Verificar palavra-passe",
+    submit: "Enviar",
+    upload: "Carregar",
+    cancel: "Cancelar",
+    save_changes: "Guardar alterações",
+    change: "Alterar",
+    subject: "Assunto",
+    message: "Mensagem",
+    saving: "A guardar...",
+    edit: "Editar",
+    more_options: "Mais opções",
+    open_link: "Abrir ligação",
+    hide: "Ocultar",
+    make_visible: "Tornar visível",
+    delete: "Eliminar",
+    share: "Partilhar",
+    title: "Título",
+    subtitle: "Subtítulo",
+    content: "Conteúdo",
+    save: "Guardar",
+    button: "Botão",
+    card: "Cartão",
+    go_back: "Voltar atrás",
+    next: "Seguinte",
+    choose_image: "Escolher imagem",
+    soon: "Em breve",
+    coming_soon_with_emoji: "Em breve 🤫",
+    no_image: "Sem imagem",
+    description: "Descrição",
+    price: "Preço",
+    duration: "Duração",
+    location: "Localização",
+    schedule: "Agenda",
+    participants_info: "Informação dos participantes",
+    description_placeholder: "Insira a descrição",
+    title_placeholder: "Insira o título",
+    changes_saved: "Alterações guardadas",
+    replace: "Substituir",
+    loading: "A carregar...",
+    image_deleted: "A imagem foi eliminada",
+    discover_workoutcool: "Descubra o Workout Cool",
+    received_just_now: "Recebido agora mesmo",
+    copied: "Copiado",
+    url_copied: "A URL foi copiada",
+    copy_failed: "Falha ao copiar",
+    accordion: "Acordeão",
+    image: "Imagem",
+    other: "Outro",
+    register: "Registar",
+    instantly: "instantaneamente",
+    immediately: "imediatamente",
+    link: "Ligação",
+    accept: "Aceitar",
+    deny: "Negar",
+    invalid_input: "Entrada inválida. Por favor, verifique os erros.",
+    copy_url: "Copiar URL",
+    page_url: "URL da página",
+    saving_short: "A guardar...",
+    saved_short: "OK",
+    looks_like_you_are_lost: "Parece que está perdido",
+    the_page_you_are_looking_for_is_not_available:
+      "A página que procura não está disponível",
+    go_to_home: "Ir para o início",
+    go_to_profile: "Ir para o perfil",
+    terms: "Termos de Serviço",
+    privacy: "Política de Privacidade",
+    sales_terms: "Termos de Venda",
+    consent_banner:
+      "Utilizamos cookies para melhorar a sua experiência. Ao clicar em Aceitar, concorda com a nossa utilização de cookies.",
+    about: "Sobre nós",
+    profile: "Perfil",
+    donate: "Doar",
+    my_account: "A minha conta",
+    dashboard: "Dashboard",
+    home: "Início",
+    changelog: "Histórico de alterações",
+    stop_impersonation_button: "Parar personificação",
+    impersonating_user_label: "Personificando utilizador",
+    re_hello: "Re Olá",
+    back_to_login: "Voltar para Login",
+    sending: "A enviar...",
+    send_me_link: "Enviar-me ligação",
+    extremely_dissatisfied: "Extremamente insatisfeito",
+    somewhat_dissatisfied: "Ligeiramente insatisfeito",
+    neutral: "Neutro",
+    satisfied: "Satisfeito",
+    support: "Suporte",
+    change_language: "Alterar idioma",
+    in_progress: "Em progresso",
+  },
+} as const;

+ 1 - 0
locales/server.ts

@@ -6,4 +6,5 @@ export const { getI18n, getScopedI18n, getStaticParams } = createI18nServer({
   es: () => import("./es"),
   "zh-CN": () => import("./zh-CN"),
   ru: () => import("./ru"),
+  pt: () => import("./pt")
 });

+ 1 - 1
middleware.ts

@@ -18,7 +18,7 @@ function detectUserLocale(request: NextRequest): string {
     .sort((a, b) => b.quality - a.quality);
 
   // Map browser locales to supported locales
-  const supportedLocales = ["en", "fr", "es", "zh-cn", "ru"];
+  const supportedLocales = ["en", "fr", "es", "zh-cn", "ru", "pt"];
 
   for (const { locale } of languages) {
     // Exact match

+ 4 - 1
src/widgets/language-selector/language-selector.tsx

@@ -12,6 +12,7 @@ const languageFlags: Record<string, string> = {
   es: "🇪🇸",
   "zh-CN": "🇨🇳",
   ru: "🇷🇺",
+  pt: "🇵🇹"
 };
 
 export function LanguageSelector() {
@@ -25,7 +26,7 @@ export function LanguageSelector() {
     document.cookie = `detected-locale=${newLocale}; max-age=${60 * 60 * 24 * 365}; path=/; samesite=lax`;
 
     // change locale immediately for better UX
-    changeLocale(newLocale as "en" | "fr" | "es" | "zh-CN" | "ru");
+    changeLocale(newLocale as "en" | "fr" | "es" | "zh-CN" | "ru" | "pt");
 
     // save to database (fire and forget)
     action.execute({ locale: newLocale });
@@ -43,6 +44,8 @@ export function LanguageSelector() {
         return "中文";
       case "ru":
         return "Русский";
+      case "pt":
+        return "Português"
       default:
         return language;
     }