脏话手册 - 1

本文详细介绍了基于英国Monty Python组合的一集喜剧《匈牙利脏话手册》的应用开发,主要功能是将用户输入翻译成短剧里的台词。通过解析XML资源文件中的英语短语,并使用StringSanitiser类处理用户输入,实现对不同输入的统一翻译。文章讨论了翻译机制、处理特殊字符和标点符号的方法,以及如何提供多种语言版本供用户选择。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简单介绍。这是个小应用,源于英国 Monty Python组合的一集喜剧
匈牙利脏话手册
剧情:一个匈牙利人拿着一本匈牙利-英文短语手册去买香烟,闹出了笑话。

原文链接: 脏话手册


2015年4.1号的时候我在Google Play上发布了一个应用,叫脏话手册。这个手册里的内容是基于Monty Python喜剧组合 的一集短片-匈牙利脏话手册里的剧情。这个系列的文章在代码中有表现,这些代码都会在最后一篇文章里发出来。在第一篇文章,我先谈谈翻译机制,如何把用户输入的内容准确重复的翻译成短剧里的短语。

有些朋友可能对匈牙利脏话手册这一集的剧情还不熟,直接就和你说这手册讲什么的,你可能会睡着,先来看一点有趣的翻译。比如说这一句,”请问车站怎么走?”,其实真实的意思是”来摸我屁股”。这个应用的主要功能就是把用户输入的字符翻译成短剧里的台词。我得到了一些志愿翻译朋友的帮助(这些朋友所作贡献我写在了文章结尾),他们帮我把脏话翻译成了各种各样的版本。所以用户可以自由的选择语言版本。

首先,我从短剧里挑了九句话(英语版本的),这九句话以后还可能会翻译成各种各样的版本。

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="Typos,UnusedResources">
  <string-array name="en">
    <item>I will not buy this record, it is scratched.</item>
    <item>My hovercraft is full of eels.</item>
    <item>Do you want to come back to my place, bouncy bouncy?</item>
    <item>If I said you had a beautiful body, would you hold it against me?</item>
    <item>I am no longer infected.</item>
    <item>You have beautiful thighs.</item>
    <item>Drop your panties, Sir William, I cannot wait till lunchtime.</item>
    <item>Please fondle my bum.</item>
    <item>My nipples explode with delight.</item>
  </string-array>
</resources>

关键在于只要用户输入相同的内容,返回的翻译结果一定要一致。基础机制就是获得用户输入的字符串的hash值,然后用这个哈希值的模来除以目标字符串的序号。这个序号总是在0-8之间(因为总共才9句话),这个序列号是用来标记那些脏话短语的,这些短语会作为翻译结果显示给用户看。

这里可能有点复杂的是,我想对用户输入内容的细微差异作一些处理。常见的就是对capitalisation,标点,还有字符串的空格的处理。不太常见的就是判断字符中是否包含变音符号,(比如说意大利语里的‘comprerò’)

我创建了一个工具类叫StringSanitiser。这个类会对输入的字符做一些基本的变换,尽可能移除字符里的干扰部分。看代码

public final class StringSanitser {
    private static final String EMPTY_STRING = "";
    private static final String SPACE = " ";
    private static final String NON_LETTERS = "\\p{Punct}";
    private static final String ACCENTS_PATTERN_STRING = "\\p{M}";
    private static final String MULTIPLE_SPACES_PATTERN_STRING = "\\s{2,}";
    private static final Pattern ACCENTS_PATTERN = Pattern.compile(ACCENTS_PATTERN_STRING);
    private static final Pattern MULTIPLE_SPACES_PATTERN = Pattern.compile(MULTIPLE_SPACES_PATTERN_STRING);

    private StringSanitser() {
        //NO-OP
    }
    //处理的方法
    public static String sanitise(String string) {
        String sanitised = normaliseCase(string);
        sanitised = removeDiactitics(sanitised);
        sanitised = removeNonLetters(sanitised);
        sanitised = removeMultipleSpaces(sanitised);
        return sanitised.trim();
    }
    //变小写
    static String normaliseCase(String string) {
        return string.trim().toLowerCase(Locale.getDefault());
    }

    //去掉标点符号
    static String removeNonLetters(String string) {
        return string.replaceAll(NON_LETTERS, EMPTY_STRING);
    }
    //去掉变音符号
    static String removeDiacritics(String string) {
        String normalised = Normalizer.normalize(string, Normalizer.Form.NFD);
        return ACCENTS_PATTERN.matcher(normalised).replaceAll(EMPTY_STRING);
    }
    //去掉空格
    static String removeMultipleSpaces(String string) {
        return MULTIPLE_SPACES_PATTERN.matcher(string).replaceAll(SPACE);
    }
}

大部分的变换都挺简单的。字符都换小写,然后用了一个正则表达式去掉标点符号-这个正则表达式 \p{Punct}会匹配所有的标点符号,然后用空字符替换他们。除此以外,我还用了一个\s{2,}表达式,这个表达式是匹配 2个或者2个以上的空格的,然后把他们都替换成空字符。这样就可以去掉所有的重复空格了。

值得一提的是,我们在removeDiacritics()方法是怎样处理变音符号的。这部分没什么特厉害的代码,不过这可是干货,很有用。想要知道这个方法的原理,那得说说一些UTF编码格式是怎样来表示变音符号的。

UTF支持变长字符编码,在UTF-8里面,每个字符都是8位的。不过很多位是根据序列里首位的值组合起来的。首位是其他单元的特征值。一般那些值小于0x7F的英文字符都可以用ASCII表示,但是更大的值都是组合起来的,这些值都超过8bit.(有待商酌)。

UTF以两种明显的方式支持变音字符

首先你可以用单个的前缀字符,这个字符是来表示符号-标点或者说变音符号。举个例子,‘ò’在UTF-8中可以用 0xC3 0xB2 当做前缀字符(带着着重符号的拉丁小写o)。注意我们之前提到的2个字符单元编码。

第二种方式是在标点符号后面加上字符(这会更改前置字符-上面提到的两种符号,首先是标点符号,然后在上面组合一个字符)。举个例子,‘ò’用UTF-8表示就是 0x6F(小写的拉丁字母o)后面跟着一个 0xCC 0x80 ‘’ (重音符)。

搞清楚这两种边长编码的区别(我没搞清)(它把多个字符单元识别为一个字符),以及组合多个可能是变长的分开的字符为一个表示它的字符 是很重要的。

有了上面的解释你应该有点清楚我们是怎样轻松的去掉变音字符了吧:把所有的变音字符都转为上面提到的第二种形式,然后去掉组合字符,这样就能把‘ò’ 转为 ‘o’。在这里我得感谢java.text.Normalizer这个类,能够做在这两种形式之间作转换。在我们的例子里用了一个小魔法把前缀字符转为我们想要的字符。有了想要的字符后,我们就能够去掉组合字符了(在代码里用了\p{M} 这个正则表达式)。

private static final String EMPTY_STRING = "";
private static final String ACCENTS_PATTERN_STRING = "\\p{M}";
private static final Pattern ACCENTS_PATTERN = Pattern.compile(ACCENTS_PATTERN_STRING);

static String removeDiacritics(String string) {
    String normalised = Normalizer.normalize(string, Normalizer.Form.NFD);
    return ACCENTS_PATTERN.matcher(normalised).replaceAll(EMPTY_STRING);
}

我做了一点单元测试来保证代码执行的结果正是我想要的。还用了一点退回测试确保这些改变没有破坏任何东西。

值得一提的是这个技术Java里都能用(去掉变音符号),并非只能用在Android上。

所以说StringSanitiser.sanitise()这个方法是做了一些简单化和标准化的处理,它会删除用户输入内容的一些干扰。但是这个方法也不是万能的,这个方法不能识别相同意思的句子。比如说”你能告诉我车站在哪里么” ,”请你帮我指出车站在哪里?”(这两句话会被翻译成不同的内容,因为处理的版本是不同的,有着不同的hash值)
不过这没关系。

在下一篇文章里,我们来谈谈翻译机制自己到底是怎么一回事,再来看看我想要加入的更多的翻译需求。

这个系列文章的最后,我会发布源代码。

我深深的感谢团队里优秀的那帮人能够好心的抽出时间帮我翻译,让我这个项目做的更好。
巴拉巴拉。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值