feat: add a translate stack

This commit is contained in:
Kharec committed 2026-10-01 10:15:28 +02:00
1 parent 5cf16588c1
commit cb356ff8c5
10 files changed
+2996

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
package translate
import (
"strings"
"unicode"
)
// The standard terms of French scripts and their Hollywood equivalents.
// Keys are folded (see fold): upper case, no accents, single spaces.
var frenchTimes = table(map[string]string{
"JOUR": "DAY", "NUIT": "NIGHT", "SOIR": "EVENING", "SOIREE": "EVENING", "MATIN": "MORNING",
"MATINEE": "MORNING", "APRES-MIDI": "AFTERNOON", "APRES MIDI": "AFTERNOON", "AUBE": "DAWN",
"AURORE": "DAWN", "CREPUSCULE": "DUSK", "COUCHER DE SOLEIL": "SUNSET", "COUCHER DU SOLEIL": "SUNSET",
"LEVER DE SOLEIL": "SUNRISE", "LEVER DU SOLEIL": "SUNRISE", "MIDI": "NOON", "MINUIT": "MIDNIGHT",
"PLUS TARD": "LATER", "UN PEU PLUS TARD": "A LITTLE LATER", "PEU APRES": "MOMENTS LATER",
"INSTANTS PLUS TARD": "MOMENTS LATER", "QUELQUES INSTANTS PLUS TARD": "MOMENTS LATER",
"QUELQUES MINUTES PLUS TARD": "MINUTES LATER", "CONTINU": "CONTINUOUS", "CONTINUE": "CONTINUOUS",
"CONTINUITE": "CONTINUOUS", "EN CONTINU": "CONTINUOUS", "SUITE": "CONTINUOUS",
"MEME MOMENT": "SAME TIME", "AU MEME MOMENT": "SAME TIME", "MEME TEMPS": "SAME TIME",
"LE LENDEMAIN": "THE NEXT DAY", "LENDEMAIN": "NEXT DAY", "PETIT MATIN": "EARLY MORNING",
"TOT LE MATIN": "EARLY MORNING", "FIN D'APRES-MIDI": "LATE AFTERNOON", "FIN DE JOURNEE": "LATE AFTERNOON",
"DEBUT DE SOIREE": "EARLY EVENING", "TARD LE SOIR": "LATE NIGHT", "TARD DANS LA NUIT": "LATE NIGHT",
"PLUS TOT": "EARLIER", "NUIT AMERICAINE": "DAY FOR NIGHT",
})
var englishTimes = table(map[string]string{
"DAY": "", "NIGHT": "", "MORNING": "", "EVENING": "", "AFTERNOON": "", "DAWN": "", "DUSK": "",
"SUNSET": "", "SUNRISE": "", "NOON": "", "MIDNIGHT": "", "LATER": "", "CONTINUOUS": "",
"MOMENTS LATER": "", "SAME TIME": "", "EARLY MORNING": "", "LATE NIGHT": "", "EARLIER": "",
"MAGIC HOUR": "", "LATE AFTERNOON": "", "EARLY EVENING": "", "SAME": "", "CONT'D": "",
})
// sceneNotes are the parenthetical notes that follow a slugline.
var sceneNotes = table(map[string]string{
"FLASH-BACK": "FLASHBACK", "FLASHBACK": "FLASHBACK", "RETOUR EN ARRIERE": "FLASHBACK",
"FIN DU FLASH-BACK": "END FLASHBACK", "FIN DU FLASHBACK": "END FLASHBACK", "REVE": "DREAM",
"CAUCHEMAR": "NIGHTMARE", "SOUVENIR": "MEMORY", "PRESENT": "PRESENT DAY", "RETOUR AU PRESENT": "PRESENT DAY",
"EN MOUVEMENT": "MOVING", "ROULANT": "MOVING", "A L'ARRET": "STATIONARY", "IMAGINAIRE": "IMAGINED",
"FANTASME": "FANTASY", "STOCK SHOT": "STOCK FOOTAGE", "IMAGES D'ARCHIVE": "STOCK FOOTAGE",
})
// extensions are the character extensions, folded without dots, spaces,
// hyphens or apostrophes: "V.O.", "VOIX OFF" and "voix-off" all give "VOIXOFF".
var extensions = map[string]string{
"VO": "V.O.", "VOIXOFF": "V.O.", "OFF": "V.O.", "ENOFF": "V.O.", "ENPENSEE": "V.O.", "PENSEE": "V.O.",
"PENSEES": "V.O.", "HC": "O.S.", "HORSCHAMP": "O.S.", "HORSCHAMPS": "O.S.", "SUITE": "CONT'D",
"CONT": "CONT'D", "ENSUITE": "CONT'D", "AUTELEPHONE": "ON PHONE", "TELEPHONE": "ON PHONE",
"AUTEL": "ON PHONE", "TEL": "ON PHONE", "ALARADIO": "ON RADIO", "RADIO": "ON RADIO",
"ALATELE": "ON TV", "ALATELEVISION": "ON TV", "TELE": "ON TV", "ALATV": "ON TV",
"SOUSTITRE": "SUBTITLED", "SOUSTITRES": "SUBTITLED", "SOUSTITREE": "SUBTITLED", "PRELAP": "PRE-LAP",
"CHUCHOTE": "WHISPERING", "CHUCHOTANT": "WHISPERING", "ENCHUCHOTANT": "WHISPERING",
"ENREGISTREMENT": "RECORDED", "ENREGISTRE": "RECORDED", "SURECRAN": "ON SCREEN", "ALECRAN": "ON SCREEN",
}
var transitions = table(map[string]string{
"COUPE": "CUT TO:", "COUPE SUR": "CUT TO:", "COUPURE": "CUT TO:", "CUT SUR": "CUT TO:",
"FONDU ENCHAINE": "DISSOLVE TO:", "ENCHAINE": "DISSOLVE TO:", "ENCHAINE SUR": "DISSOLVE TO:",
"FONDU ENCHAINE SUR": "DISSOLVE TO:", "FONDU ENCHAINE A": "DISSOLVE TO:",
"FONDU AU NOIR": "FADE TO BLACK.", "FONDU EN NOIR": "FADE TO BLACK.", "FONDU NOIR": "FADE TO BLACK.",
"FONDU AU BLANC": "FADE TO WHITE.", "FONDU EN FERMETURE": "FADE OUT.", "FERMETURE AU NOIR": "FADE OUT.",
"FERMETURE EN FONDU": "FADE OUT.", "FONDU DE FERMETURE": "FADE OUT.", "FONDU DE SORTIE": "FADE OUT.",
"FONDU EN OUVERTURE": "FADE IN:", "OUVERTURE AU NOIR": "FADE IN:", "OUVERTURE EN FONDU": "FADE IN:",
"FONDU D'OUVERTURE": "FADE IN:", "OUVERTURE EN FONDU AU NOIR": "FADE IN:", "OUVERTURE": "FADE IN:",
"COUPE FRANCHE": "SMASH CUT TO:", "CUT FRANC": "SMASH CUT TO:", "COUPE SECHE": "SMASH CUT TO:",
"CUT SEC": "SMASH CUT TO:", "RACCORD": "MATCH CUT TO:", "RACCORD SUR": "MATCH CUT TO:",
"RACCORD MOUVEMENT": "MATCH CUT TO:", "RACCORD REGARD": "MATCH CUT TO:", "RACCORD DANS L'AXE": "MATCH CUT TO:",
"COUPE SAUTEE": "JUMP CUT TO:", "VOLET": "WIPE TO:", "VOLET SUR": "WIPE TO:", "FONDU": "FADE TO:",
"FONDU SUR": "FADE TO:", "FONDU A": "FADE TO:", "FIN": "THE END",
})
// shots are matched as the first words of an insert / shot element; the
// English forms start like the shots the Fountain format recognises.
var shots = []struct{ french, english string }{
{"TRES GROS PLAN SUR", "EXTREME CLOSE ON"},
{"TRES GROS PLAN", "EXTREME CLOSE-UP"},
{"GROS PLAN SUR", "CLOSE ON"},
{"GROS PLAN", "CLOSE UP"},
{"PLAN RAPPROCHE SUR", "CLOSE ON"},
{"PLAN SERRE SUR", "CLOSE ON"},
{"PLAN LARGE SUR", "WIDE ON"},
{"PLAN LARGE", "WIDE SHOT"},
{"PLAN D'ENSEMBLE", "WIDE SHOT"},
{"PLAN GENERAL", "WIDE SHOT"},
{"PLAN DE SITUATION", "WIDE SHOT"},
{"PLAN AERIEN", "AERIAL SHOT"},
{"VUE AERIENNE", "AERIAL SHOT"},
{"PLAN EN PLONGEE", "OVERHEAD SHOT"},
{"PLONGEE", "OVERHEAD SHOT"},
{"VUE DU DESSUS", "OVERHEAD SHOT"},
{"POINT DE VUE DE", "POV"},
{"POINT DE VUE", "POV"},
{"P.D.V.", "POV"},
{"PDV", "POV"},
{"PLAN SUBJECTIF", "POV"},
{"VUE SUBJECTIVE", "POV"},
{"CONTRECHAMP", "REVERSE ANGLE"},
{"CONTRE-CHAMP", "REVERSE ANGLE"},
{"NOUVEL ANGLE", "NEW ANGLE"},
{"ANGLE SUR", "ANGLE ON"},
{"RETOUR A LA SCENE", "BACK TO SCENE"},
{"RETOUR SUR LA SCENE", "BACK TO SCENE"},
{"INSERT SUR", "INSERT"},
{"TRAVELLING SUR", "TRACKING ON"},
{"TRAVELLING", "TRACKING SHOT"},
{"PLAN MOYEN", "MEDIUM SHOT"},
{"PLAN AMERICAIN", "MEDIUM SHOT"},
{"PLAN TAILLE", "MEDIUM SHOT"},
{"SERIE DE PLANS", "SERIES OF SHOTS"},
{"FIN DU MONTAGE", "END MONTAGE"},
}
// parentheticals are the stock wrylies, folded to lower case.
var parentheticals = map[string]string{
"un temps": "beat", "temps": "beat", "un temps d'arret": "beat", "un silence": "beat",
"suite": "continuing", "continuant": "continuing", "a voix basse": "quietly", "tout bas": "quietly",
"bas": "quietly", "a part": "aside", "ironique": "wryly", "pour lui-meme": "to himself",
"pour elle-meme": "to herself", "pour soi": "to themself", "off": "O.S.", "en off": "V.O.",
"hors champ": "O.S.", "hors-champ": "O.S.", "voix off": "V.O.",
}
// credits are the usual title page credits, folded to lower case.
var credits = map[string]string{
"ecrit par": "Written by", "scenario de": "Screenplay by", "scenario": "Screenplay by",
"scenario et dialogues de": "Screenplay by", "scenario et dialogues": "Screenplay by",
"un film de": "A film by", "realise par": "Directed by", "d'apres une idee de": "Story by",
"histoire originale de": "Story by",
}
func table(m map[string]string) map[string]string {
out := make(map[string]string, len(m))
for k, v := range m {
out[fold(k)] = v
}
return out
}
var accents = strings.NewReplacer(
"À", "A", "Â", "A", "Ä", "A", "Ç", "C", "É", "E", "È", "E", "Ê", "E", "Ë", "E", "Î", "I", "Ï", "I",
"Ô", "O", "Ö", "O", "Ù", "U", "Û", "U", "Ü", "U", "Œ", "OE", "Æ", "AE", "Ÿ", "Y", "’", "'",
)
// fold upper-cases, removes accents and collapses spaces, to look a term
// up whatever its spelling.
func fold(s string) string {
return strings.Join(strings.Fields(accents.Replace(strings.ToUpper(s))), " ")
}
// foldTight also drops dots, spaces, hyphens and apostrophes ("V.O." → "VO").
func foldTight(s string) string {
return strings.Map(func(r rune) rune {
if r == '.' || r == '-' || r == '\'' || unicode.IsSpace(r) {
return -1
}
return r
}, fold(s))
}
// foldLower is fold in lower case, for parentheticals and credits.
func foldLower(s string) string { return strings.ToLower(fold(s)) }