From 373c218eda13309ef4d20760e803219c8f43f504 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 1 Sep 2026 00:07:58 +0000 Subject: [PATCH] Add Portuguese as UI and transcription language Register pt for interface localization (web + Electron menus + NSIS), transcript language selection with MMS forced alignment, and conservative Portuguese filler words, with matching tests. Co-authored-by: Wassim Gharbi --- electron/locale/catalogs.ts | 2 + electron/locale/pt.ts | 43 +++++++ lib/alignModels.ts | 4 + lib/fillers.ts | 10 ++ lib/i18n/catalogs.ts | 2 + lib/i18n/locales.ts | 8 ++ lib/i18n/messages/pt.ts | 218 ++++++++++++++++++++++++++++++++++++ lib/languages.ts | 10 +- package.json | 3 +- tests/align-models-test.ts | 2 + tests/fillers-test.ts | 9 +- tests/i18n-test.ts | 14 ++- tests/languages-test.ts | 6 +- 13 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 electron/locale/pt.ts create mode 100644 lib/i18n/messages/pt.ts diff --git a/electron/locale/catalogs.ts b/electron/locale/catalogs.ts index 2f86bbf..3effbb5 100644 --- a/electron/locale/catalogs.ts +++ b/electron/locale/catalogs.ts @@ -5,6 +5,7 @@ import { es } from "./es"; import { fr } from "./fr"; import { ja } from "./ja"; import { ko } from "./ko"; +import { pt } from "./pt"; import { zhCN } from "./zh-CN"; import { zhTW } from "./zh-TW"; @@ -19,4 +20,5 @@ export const desktopCatalogs: Record = { es, fr, de, + pt, }; diff --git a/electron/locale/pt.ts b/electron/locale/pt.ts new file mode 100644 index 0000000..100eb41 --- /dev/null +++ b/electron/locale/pt.ts @@ -0,0 +1,43 @@ +import type { DesktopMessageKey } from "./en"; + +export const pt: Record = { + file: "Arquivo", + openProject: "Abrir projeto…", + reopenLast: "Reabrir o último projeto", + recentProjects: "Projetos recentes", + noRecent: "Não há projetos recentes", + clearRecent: "Limpar projetos recentes", + edit: "Editar", + undo: "Desfazer", + redo: "Refazer", + cut: "Cortar", + copy: "Copiar", + paste: "Colar", + pasteMatch: "Colar e combinar formatação", + delete: "Excluir", + selectAll: "Selecionar tudo", + view: "Exibir", + reload: "Recarregar", + forceReload: "Forçar recarga", + devTools: "Alternar ferramentas de desenvolvedor", + resetZoom: "Tamanho real", + zoomIn: "Aproximar", + zoomOut: "Afastar", + fullscreen: "Alternar tela cheia", + window: "Janela", + minimize: "Minimizar", + zoom: "Zoom", + front: "Trazer tudo para a frente", + close: "Fechar janela", + quit: "Sair do Rescript", + about: "Sobre o Rescript", + services: "Serviços", + hide: "Ocultar Rescript", + hideOthers: "Ocultar outros", + unhide: "Mostrar tudo", + restart: "Reiniciar", + later: "Mais tarde", + updateTitle: "Atualização disponível", + updateMessage: "Rescript {version} está pronto para ser instalado.", + updateDetail: "Reinicie agora para aplicar a atualização.", +}; diff --git a/lib/alignModels.ts b/lib/alignModels.ts index b94e808..3145c72 100644 --- a/lib/alignModels.ts +++ b/lib/alignModels.ts @@ -36,6 +36,10 @@ export const ALIGN_MODELS: Partial< id: "onnx-community/mms-300m-1130-forced-aligner-ONNX", normalize: "latin-lower", }, + pt: { + id: "onnx-community/mms-300m-1130-forced-aligner-ONNX", + normalize: "latin-lower", + }, zh: { id: "onnx-community/wav2vec2-large-xlsr-53-chinese-zh-cn-ONNX", normalize: "cjk", diff --git a/lib/fillers.ts b/lib/fillers.ts index 3216b46..c9b5a0e 100644 --- a/lib/fillers.ts +++ b/lib/fillers.ts @@ -43,6 +43,16 @@ const FILLER_WORDS = new Set([ "emm", "emmm", "eee", + // Portuguese + "hã", + "hãhã", + "ahn", + "ahnn", + "ãh", + "ãhh", + "ãhm", + "éh", + "éhh", // Chinese "嗯", "嗯嗯", diff --git a/lib/i18n/catalogs.ts b/lib/i18n/catalogs.ts index 165b0d2..ad39d4f 100644 --- a/lib/i18n/catalogs.ts +++ b/lib/i18n/catalogs.ts @@ -4,6 +4,7 @@ import { es } from "./messages/es"; import { fr } from "./messages/fr"; import { ja } from "./messages/ja"; import { ko } from "./messages/ko"; +import { pt } from "./messages/pt"; import { zhCN } from "./messages/zh-CN"; import { zhTW } from "./messages/zh-TW"; import type { UiLocale } from "./locales"; @@ -19,4 +20,5 @@ export const catalogs: Record = { es, fr, de, + pt, }; diff --git a/lib/i18n/locales.ts b/lib/i18n/locales.ts index 640e1b5..a022791 100644 --- a/lib/i18n/locales.ts +++ b/lib/i18n/locales.ts @@ -14,6 +14,7 @@ export const UI_LOCALES = [ "es", "fr", "de", + "pt", ] as const; export type UiLocale = (typeof UI_LOCALES)[number]; @@ -92,6 +93,12 @@ export const UI_LOCALE_META: Record = { nsis: "de_DE", match: prefix("de"), }, + pt: { + htmlLang: "pt", + nativeLabel: "Português", + nsis: "pt_BR", + match: prefix("pt"), + }, }; function isTraditionalChinese(tag: string): boolean { @@ -111,6 +118,7 @@ const MATCH_ORDER: readonly UiLocale[] = [ "es", "fr", "de", + "pt", "en", ]; diff --git a/lib/i18n/messages/pt.ts b/lib/i18n/messages/pt.ts new file mode 100644 index 0000000..53125cf --- /dev/null +++ b/lib/i18n/messages/pt.ts @@ -0,0 +1,218 @@ +import type { MessageKey } from "./en"; + +/** Portuguese UI catalog. Every key in {@link en} must be present. */ +export const pt: Record = { + "app.title": "Rescript — edite vídeos como edita texto", + "common.cancel": "Cancelar", + "common.close": "Fechar", + "common.delete": "Excluir", + "common.download": "Baixar", + "common.import": "Importar", + "common.loading": "Carregando", + "common.remove": "Remover", + "common.restore": "Restaurar", + "common.retry": "Tentar de novo", + "common.searchOrCreate": "Buscar ou criar…", + "common.settings": "Configurações", + "common.system": "Sistema", + "common.tools": "Ferramentas", + "settings.appearance": "Aparência", + "settings.light": "Claro", + "settings.dark": "Escuro", + "settings.interfaceLanguage": "Idioma da interface", + "settings.privacy": "Privacidade", + "settings.helpImprove": "Ajude a melhorar o app", + "settings.telemetryHelp": "Envia estatísticas anônimas de uso de recursos e relatórios de falhas.", + "settings.support": "Suporte / feedback", + "settings.reportIssue": "Reportar um problema", + "settings.homepage": "Página inicial", + "settings.github": "GitHub", + "settings.followX": "Seguir no X", + "model.transcriptSource": "Origem da transcrição", + "model.language": "Idioma", + "model.transcriptLanguage": "Idioma da transcrição", + "model.importTranscript": "Importar transcrição", + "upload.recentProjects": "Projetos recentes", + "upload.removeRecent": "Remover dos recentes", + "upload.dropPrefix": "Solte um arquivo de vídeo ou áudio aqui, ou", + "upload.browse": "navegue", + "upload.chooseTranscriptFirst": "Escolha uma transcrição no menu acima e depois solte a mídia", + "upload.willUseTranscript": "Usará {name} · MP4, WebM, MOV, MP3, WAV, …", + "upload.mediaFormats": "MP4, WebM, MOV, MP3, WAV, M4A, …", + "upload.gettingReady": "Preparando tudo", + "upload.gettingReadyHelp": "Configurando o motor de mídia; isso só acontece uma vez.", + "upload.unsupported": "Este navegador não pode executar o editor", + "upload.unsupportedHelp": + "A edição precisa de SharedArrayBuffer, que exige uma página isolada entre origens. Experimente uma versão recente do Chrome, Edge, Safari ou Firefox via HTTPS.", + "upload.transcribeTitle": "Transcrever", + "upload.transcribeText": "Whisper localmente, ou importe SRT / VTT.", + "upload.editTitle": "Editar", + "upload.editText": "Selecione palavras e pressione Excluir para editar.", + "upload.exportTitle": "Exportar", + "upload.exportText": "Renderize o corte final em MP4 ou M4A.", + "editor.chooseTranscript": "Primeiro escolha um arquivo de transcrição no menu de origem.", + "editor.chooseMedia": "Escolha um arquivo de vídeo ou áudio.", + "editor.undo": "Desfazer (⌘Z)", + "editor.redo": "Refazer (⇧⌘Z)", + "editor.export": "Exportar", + "topbar.startOver": "Começar de novo", + "transcript.replace": "Substituir transcrição a partir de SRT, VTT ou JSON", + "transcript.header": "Transcrição", + "transcript.wordDeleted": "{count} palavra", + "transcript.wordsDeleted": "{count} palavras", + "transcript.replaceConfirm": "Substituir a transcrição atual por este arquivo?", + "transcript.invalidFile": "Escolha um arquivo de transcrição SRT, VTT ou JSON.", + "transcript.noSpeech": "Nenhuma fala detectada. Confirme que este arquivo tem áudio, ou importe uma transcrição.", + "transcript.cut": "Cortar", + "transcript.follow": "Seguir cabeçote", + "transcript.hideDeleted": "Ocultar palavras excluídas", + "transcript.showDeleted": "Mostrar palavras excluídas", + "transcript.correct": "Corrigir", + "transcript.scrollWithPlayhead": "Rolar com o cabeçote", + "transcript.joinSplit": "Clipe dividido — clique para unir estes clipes", + "transcript.joinClips": "Unir clipes", + "transcript.hesitation": "Hesitação detectada (não transcrita) — corte-a com Remover vícios de linguagem", + "tools.bulk": "Limpezas em massa da transcrição", + "tools.removeFillers": "Remover vícios de linguagem", + "tools.removeFillersTitle": "Corta vícios (\"ãh\", \"mmm\", \"...\", …) do vídeo", + "tools.restoreFillers": "Restaurar vícios de linguagem", + "tools.restoreFillersTitle": "Devolve cada vício cortado", + "tools.removeSilences": "Remover silêncios", + "tools.removeSilencesTitle": "Corta pausas e silêncios (≥{seconds} s) do vídeo", + "tools.restoreSilences": "Restaurar silêncios", + "tools.restoreSilencesTitle": "Devolve cada pausa cortada", + "timeline.removed": "{trimmed} removido — duração original {duration}", + "timeline.back5": "Voltar 5 s", + "timeline.playPause": "Reproduzir / pausar (espaço)", + "timeline.forward5": "Avançar 5 s", + "timeline.split": "Dividir", + "timeline.splitTitle": "Dividir clipe no cabeçote (S)", + "timeline.splitDisabled": "Mova o cabeçote para uma região mantida para dividir", + "timeline.delete": "Excluir", + "timeline.deleteTitle": "Excluir clipe selecionado (Excluir)", + "timeline.deleteDisabled": "Selecione um clipe na linha do tempo para excluí-lo", + "timeline.restore": "Restaurar", + "timeline.restoreTitle": "Restaurar corte selecionado (Excluir)", + "timeline.restoreDisabled": "Selecione um corte ou uma seção de silêncio na linha do tempo para restaurar", + "timeline.zoomOut": "Afastar", + "timeline.fit": "Ajustar à janela", + "timeline.zoomIn": "Aproximar — arraste as bordas das palavras para ajustar o tempo", + "timeline.joinClips": "Unir estes clipes (remover divisão)", + "timeline.trimStart": "Recortar início do clipe", + "timeline.trimEnd": "Recortar final do clipe", + "timeline.hesitationAdjust": "… hesitação detectada — arraste as bordas para ajustar, ou corte-a com Remover vícios de linguagem", + "timeline.hesitationCut": "… hesitação detectada — corte-a com Remover vícios de linguagem", + "timeline.dragTiming": "{word} — arraste as bordas para ajustar o tempo", + "timeline.scrollZoom": "Role para aproximar / afastar", + "speaker.moveStart": "Arraste para mover onde este falante começa", + "speaker.moveLabel": "Mover rótulo do falante", + "speaker.options": "Opções do falante", + "speaker.change": "Alterar falante", + "speaker.rename": "Renomear falante", + "speaker.renameAction": "Renomear", + "speaker.replace": "Substituir no projeto por…", + "speaker.removeProject": "Remover do projeto", + "speaker.newName": "Novo nome", + "speaker.find": "Buscar um falante…", + "speaker.noMatches": "Nenhum falante correspondente", + "speaker.renameTo": "Renomear {current} para {next}", + "speaker.button": "Falante", + "speaker.search": "Buscar ou criar…", + "speaker.defaultName": "Falante {number}", + "speaker.create": "Criar \"{name}\"", + "export.title": "Exportar", + "export.type": "Tipo de exportação", + "export.video": "Vídeo", + "export.audio": "Áudio", + "export.transcript": "Transcrição", + "export.subtitles": "Legendas", + "export.videoUnavailable": "A exportação de vídeo não está disponível para projetos só de áudio", + "export.noAudio": "Este arquivo não tem faixa de áudio", + "export.noWordsFirst": "Transcreva ou importe uma transcrição primeiro", + "export.format": "Formato", + "export.resolution": "Resolução", + "export.original": "Original", + "export.plainText": "Texto sem formatação", + "export.statOriginal": "Original", + "export.statCuts": "Cortes", + "export.statEdited": "Editado", + "export.transcriptHelp": "Texto com rótulos de falante e cortes removidos. Sem marcas de tempo.", + "export.subtitlesHelp": + "SRT e VTT usam a linha do tempo editada (com cortes aplicados). JSON conserva a lista completa de palavras para reimportar.", + "export.encodingHelp": "Recodificando com ffmpeg.wasm — arquivos longos demoram um pouco.", + "export.reexport": "Reexportar com as últimas edições", + "export.rendering": "Renderizando no seu navegador…", + "export.downloadFile": "Baixar {name}", + "export.downloadFormat": "Baixar .{format}", + "export.exportFormat": "Exportar {format}", + "export.timeline": "Linha do tempo", + "export.timelineTitle": "Exportar uma sequência NLE (Resolve, Premiere, Final Cut, Pro Tools)", + "export.nle": "NLE", + "export.frameRate": "Taxa de quadros", + "export.timelineHelp": + "A sequência referencia a mídia original pelo nome do arquivo — vincule-a de novo no NLE após importar.", + "export.timelineHelpAaf": "AAF do Pro Tools / Logic (somente metadados).", + "export.timelineHelpFcpx": "FCPXML do Final Cut Pro.", + "export.timelineHelpPremiere": "XML do Adobe Premiere Pro (xmeml).", + "export.timelineHelpResolve": "XML do DaVinci Resolve (xmeml).", + "export.aafOverCap": + "Esta edição tem {count} clipes; AAF admite até {max}. Use Resolve, Premiere ou Final Cut.", + "export.preparing": "Preparando…", + "import.reading": "Lendo transcrição…", + "import.importing": "Importando…", + "import.failed": "Falha ao importar", + "import.readingFile": "Lendo arquivo…", + "import.chooseFileShort": "Escolha um arquivo…", + "import.chooseFile": "Escolha uma transcrição SRT, VTT ou JSON", + "import.selected": "{name} selecionado", + "banner.faster": "Quer transcrições e exportações mais rápidas?", + "banner.getDesktop": "Obtenha o app de desktop do Rescript", + "banner.downloadFor": "Baixar para {platform}", + "banner.dismiss": "Dispensar", + "social.githubRepo": "Repositório do GitHub", + "social.discordServer": "Servidor do Discord", + "social.xProfile": "Perfil do X", + "globalError.title": "Rescript — algo deu errado", + "globalError.heading": "Algo deu errado", + "globalError.body": + "O editor encontrou um erro inesperado. Seus projetos salvos ainda estão neste dispositivo; ao recarregar devem voltar.", + "progress.loadingMedia": "Carregando mídia…", + "progress.loadingMediaEngine": "Carregando motor de mídia…", + "progress.extractingAudio": "Extraindo áudio…", + "progress.loadingSpeechModel": "Carregando modelo de voz…", + "progress.loadingSpeechCache": "Carregando modelo de voz do cache…", + "progress.downloadingSpeech": "Baixando modelo de voz…", + "progress.gpuFallback": "A GPU foi interrompida — continuando na CPU…", + "progress.detectingSpeech": "Detectando fala…", + "progress.transcribing": "Transcrevendo…", + "progress.loadingAlignCache": "Carregando modelo de alinhamento do cache…", + "progress.downloadingAlign": "Baixando modelo de alinhamento…", + "progress.aligning": "Alinhando palavras…", + "progress.speakers": "Identificando falantes…", + "error.selectModel": "Selecione um modelo de voz para transcrever.", + "error.workerCrashed": "O worker de transcrição falhou.", + "error.mediaEngineNetwork": "Não foi possível carregar o motor de mídia — a conexão caiu. Verifique sua internet e tente de novo.", + "error.processFile": "Não foi possível processar este arquivo.", + "error.extractAudio": "Não foi possível extrair áudio deste arquivo.", + "error.nothingToExport": "Tudo foi excluído — não há nada para exportar.", + "error.videoExport": "A exportação falhou ao renderizar o vídeo.", + "error.audioExport": "A exportação falhou ao renderizar o áudio.", + "error.export": "Erro ao exportar.", + "error.timelineExport": "Erro ao exportar a linha do tempo.", + "error.timelineEmpty": "Não há nada para colocar na linha do tempo.", + "error.aafTemplate": "Não foi possível carregar o modelo de exportação AAF.", + "error.emptyTranscript": "Esse arquivo de transcrição está vazio.", + "error.noTimedWords": "Não foram encontradas palavras com tempo nessa transcrição.", + "error.parseJson": "Não foi possível analisar essa transcrição JSON.", + "error.jsonShape": 'JSON deve ser um array de palavras ou { "words": [...] }.', + "error.noWords": "Não há palavras para exportar.", + "error.projectMissing": "Esse projeto não está mais salvo.", + "error.openProject": "Não foi possível abrir esse projeto.", + "error.removeProject": "Não foi possível remover esse projeto.", + "error.readTranscript": "Não foi possível ler essa transcrição.", + "error.clearRecent": "Não foi possível limpar os projetos recentes.", + "error.modelDownload": + "Não foi possível terminar de baixar o modelo de voz — a conexão caiu. Verifique sua internet e tente de novo; as partes já baixadas são preservadas.", + "error.gpuReset": "A transcrição foi interrompida quando a GPU reiniciou (muitas vezes após bloquear a tela). Tente de novo.", + "confirm.clearRecent": "Remover todos os projetos recentes? As edições salvas serão excluídas.", +}; diff --git a/lib/languages.ts b/lib/languages.ts index 0074b66..52b9f4b 100644 --- a/lib/languages.ts +++ b/lib/languages.ts @@ -1,4 +1,4 @@ -export type TranscriptLanguage = "en" | "es" | "fr" | "de" | "zh"; +export type TranscriptLanguage = "en" | "es" | "fr" | "de" | "pt" | "zh"; export interface TranscriptLanguageInfo { label: string; @@ -41,6 +41,12 @@ export const TRANSCRIPT_LANGUAGES: Record< flag: "🇩🇪", code: "DE", }, + pt: { + label: "Portuguese", + nativeLabel: "Português", + flag: "🇧🇷", + code: "PT", + }, zh: { label: "Chinese", nativeLabel: "中文", @@ -54,6 +60,7 @@ export const TRANSCRIPT_LANGUAGE_ORDER: TranscriptLanguage[] = [ "es", "fr", "de", + "pt", "zh", ]; @@ -65,6 +72,7 @@ export function isTranscriptLanguage( value === "es" || value === "fr" || value === "de" || + value === "pt" || value === "zh" ); } diff --git a/package.json b/package.json index b470f71..2b4bfee 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,8 @@ "ko_KR", "es_ES", "fr_FR", - "de_DE" + "de_DE", + "pt_BR" ], "multiLanguageInstaller": true, "artifactName": "${productName}-Setup.${ext}" diff --git a/tests/align-models-test.ts b/tests/align-models-test.ts index 8ee87b1..318c4d2 100644 --- a/tests/align-models-test.ts +++ b/tests/align-models-test.ts @@ -13,6 +13,8 @@ function assert(cond: boolean, msg: string) { assert(alignModelFor("es")?.id === ALIGN_MODELS.es!.id, "es has an aligner"); assert(alignModelFor("fr")?.normalize === "latin-lower", "fr uses MMS folding"); assert(alignModelFor("de")?.id === alignModelFor("es")?.id, "eu langs share MMS"); + assert(alignModelFor("pt")?.id === alignModelFor("es")?.id, "pt shares MMS"); + assert(alignModelFor("pt")?.normalize === "latin-lower", "pt uses MMS folding"); assert(alignModelFor("zh")?.normalize === "cjk", "zh uses CJK normalize"); assert(alignModelFor("en")?.normalize === "latin-upper", "en stays uppercase"); assert(alignModelFor(undefined) === null, "missing language → no CTC"); diff --git a/tests/fillers-test.ts b/tests/fillers-test.ts index 55af32d..37f135f 100644 --- a/tests/fillers-test.ts +++ b/tests/fillers-test.ts @@ -26,6 +26,13 @@ function w(text: string, id: number, deleted = false): Word { } } +{ + const portugueseFillers = ["hã", "Ahn,", "ãhm", "éhh"]; + for (const word of portugueseFillers) { + if (!isFillerWord(word)) throw new Error(`expected ${word} to be a filler`); + } +} + { const chineseFillers = ["嗯", "呃", "额", "唔", "嗯嗯"]; for (const word of chineseFillers) { @@ -34,7 +41,7 @@ function w(text: string, id: number, deleted = false): Word { } { - const meaningfulWords = ["also", "genau", "ja", "bonjour", "hola", "这个"]; + const meaningfulWords = ["also", "genau", "ja", "bonjour", "hola", "olá", "这个"]; for (const word of meaningfulWords) { if (isFillerWord(word)) throw new Error(`did not expect ${word} to be a filler`); } diff --git a/tests/i18n-test.ts b/tests/i18n-test.ts index 847afe6..5d846c0 100644 --- a/tests/i18n-test.ts +++ b/tests/i18n-test.ts @@ -26,8 +26,10 @@ assert(resolveUiLocale("system", ["ko"]) === "ko", "korean detection"); assert(resolveUiLocale("system", ["es-MX"]) === "es", "spanish detection"); assert(resolveUiLocale("system", ["fr-CA"]) === "fr", "french detection"); assert(resolveUiLocale("system", ["de-AT"]) === "de", "german detection"); +assert(resolveUiLocale("system", ["pt-BR"]) === "pt", "portuguese detection"); +assert(resolveUiLocale("system", ["pt-PT", "en-US"]) === "pt", "portuguese ordered"); assert(resolveUiLocale("system", ["fr-FR", "en-US"]) === "fr", "ordered fallback"); -assert(resolveUiLocale("system", ["pt-BR", "en-US"]) === "en", "unsupported then en"); +assert(resolveUiLocale("system", ["xx-YY", "en-US"]) === "en", "unsupported then en"); assert(resolveUiLocale("system", []) === "en", "empty fallback"); assert(resolveUiLocale("ja", ["en-US"]) === "ja", "manual override"); assert(matchUiLocale("zh_TW") === "zh-TW", "underscore normalized"); @@ -35,6 +37,7 @@ assert(matchUiLocale("zh_TW") === "zh-TW", "underscore normalized"); assert(translate("zh-CN", "common.settings") === "设置", "Chinese settings"); assert(translate("ja", "common.settings") === "設定", "Japanese settings"); assert(translate("es", "common.settings") === "Ajustes", "Spanish settings"); +assert(translate("pt", "common.settings") === "Configurações", "Portuguese settings"); assert( translate("de", "export.downloadFile", { name: "demo.mp4" }).includes("demo.mp4"), "named interpolation" @@ -76,14 +79,15 @@ for (const locale of UI_LOCALES) { } } +const nsis = nsisInstallerLanguages(); +assert(nsis.includes("en_US") && nsis.includes("ja_JP") && nsis.includes("zh_TW"), "nsis codes"); +assert(nsis.includes("pt_BR"), "nsis includes Portuguese"); + const boot = buildLocaleBootScript(); assert(boot.includes("zh-TW"), "boot script knows Traditional Chinese"); assert(boot.includes("ja"), "boot script knows Japanese"); +assert(boot.includes("pt"), "boot script knows Portuguese"); assert(boot.includes("navigator.languages"), "boot script reads system languages"); - -const nsis = nsisInstallerLanguages(); -assert(nsis.includes("en_US") && nsis.includes("ja_JP") && nsis.includes("zh_TW"), "nsis codes"); - const now = Date.UTC(2026, 7, 9, 12, 0, 0); assert(formatRelativeTime("ja", now - 5 * 60_000, now).includes("5"), "ja relative"); assert(formatRelativeTime("en", now - 5 * 60_000, now).includes("5"), "en relative"); diff --git a/tests/languages-test.ts b/tests/languages-test.ts index b10e6b9..eeaea0b 100644 --- a/tests/languages-test.ts +++ b/tests/languages-test.ts @@ -14,15 +14,15 @@ import { if (!isTranscriptLanguage("es")) throw new Error("expected es to be valid"); if (!isTranscriptLanguage("fr")) throw new Error("expected fr to be valid"); if (!isTranscriptLanguage("de")) throw new Error("expected de to be valid"); + if (!isTranscriptLanguage("pt")) throw new Error("expected pt to be valid"); if (!isTranscriptLanguage("zh")) throw new Error("expected zh to be valid"); - if (isTranscriptLanguage("pt")) throw new Error("did not expect pt to be valid"); + if (isTranscriptLanguage("it")) throw new Error("did not expect it to be valid"); } - { const labels = TRANSCRIPT_LANGUAGE_ORDER.map( (id) => TRANSCRIPT_LANGUAGES[id].nativeLabel ); - if (labels.join(",") !== "English,Español,Français,Deutsch,中文") { + if (labels.join(",") !== "English,Español,Français,Deutsch,Português,中文") { throw new Error(`unexpected language order: ${labels.join(",")}`); } }